Skip to content
Open

Dev #12

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
3e44bde
fix: correct startup issues
jairomelo Nov 21, 2025
1ad4ec4
fix: replacing deprecated method for utc time
jairomelo Nov 21, 2025
4683862
fix: including validation for pagination parameters
jairomelo Nov 21, 2025
9c3f5d3
fix: ensure model imports are mandatory for database initialization
jairomelo Nov 21, 2025
d793f6a
fix: update DocumentRead config to use form_attributes instead of dep…
jairomelo Nov 21, 2025
c4cec28
missing foreing key constraint on document_image_id for ExifData model.
jairomelo Nov 21, 2025
68d85f0
fix: change database connection pool from NullPool to QueuePool to pr…
jairomelo Nov 21, 2025
8e85aae
fix: update list_camera_settings to use Query parameters for pagination
jairomelo Nov 21, 2025
5e1e371
fix: handle IntegrityError during document creation to prevent crashes
jairomelo Nov 21, 2025
adfde6b
fix: add document existence check and handle IntegrityError in create…
jairomelo Nov 21, 2025
f85e0e5
fix: correct module import
jairomelo Nov 21, 2025
fb15db2
fix: update Config class to use form_attributes instead of orm_mode i…
jairomelo Nov 21, 2025
bdf24d8
fix: remove unused import of os in db.py
jairomelo Nov 21, 2025
68cec85
fix: add .python-version file to manage versioning with pyenv
jairomelo Nov 21, 2025
1ad61a1
feat: add Alembic configuration and initial migration setup
jairomelo Nov 21, 2025
7cda45c
added missing fields in class settings of app/core/config.py (UVCORN_…
Asemica-me Dec 1, 2025
214b4ac
Add user authentication, projects, and document enhancements
Asemica-me Dec 17, 2025
9e34eb9
feat: add configuration settings and initial service implementation f…
jairomelo Jan 4, 2026
143cdf2
feat: add dual shoot script for simultaneous image capture with logging
jairomelo Jan 4, 2026
cf96694
refactor: remove test project creation and subprocess call from servi…
jairomelo Jan 4, 2026
d8876a7
feat: update .gitignore to include _data/, _logs/, and *.pdf
jairomelo Jan 4, 2026
91c808e
feat: implement CameraConfig class and update capture functions for i…
jairomelo Jan 4, 2026
fa0c821
feat: enhance configuration settings and improve logging in capture s…
jairomelo Jan 4, 2026
aa9223c
fix: import Path
jairomelo Jan 4, 2026
6cd3182
feat: add encoding and raw capture options to CameraConfig and update…
jairomelo Jan 4, 2026
a91aaed
feat: add image encoding parameter to filename generation and capture…
jairomelo Jan 4, 2026
7c81d87
feat: refactor CameraConfig as module for better readability and main…
jairomelo Jan 4, 2026
1f4eb61
feat: add app_version field to Settings for version tracking
jairomelo Jan 4, 2026
fadac10
feat: add CaptureFile, CaptureCamera, and CaptureRecord dataclasses f…
jairomelo Jan 4, 2026
ab3e15e
feat: implement generate_manifest_record and append_manifest_record f…
jairomelo Jan 4, 2026
f33f088
feat: update CameraConfig to include timeout and quality parameters f…
jairomelo Jan 4, 2026
619b6c8
feat: implement single_capture_image function for capturing images fr…
jairomelo Jan 4, 2026
c2cc9cc
feat: add compute_sha256 utility function for file hash computation a…
jairomelo Jan 4, 2026
90a9579
feat:move manifest helper functions to manifestHandler module. Create…
jairomelo Jan 4, 2026
819901e
feat: add camera backend abstraction and implement subprocess backend…
jairomelo Jan 4, 2026
403a67a
fix: add missing newline at end of file in append_manifest_record fun…
jairomelo Jan 4, 2026
6ec3cb3
feat: update camera backend to support Picamera2 and modify settings …
jairomelo Jan 4, 2026
24e9688
feat: add dependencies documentation and setup script for camera back…
jairomelo Jan 4, 2026
011deea
fix: optimize still configuration creation in Picamera2 backend
jairomelo Jan 4, 2026
19d7be8
refactor: adjust import statements and sys.path handling for better m…
jairomelo Jan 4, 2026
2503c6e
feat: add pytest configuration and test fixtures for camera functiona…
jairomelo Jan 4, 2026
d709186
fix: update requirements.txt to ensure pytest is included and clean u…
jairomelo Jan 4, 2026
ca8d6bf
fix: remove print statements from dual capture test
jairomelo Jan 4, 2026
39d02ff
feat: implement command-line tool for dual camera calibration
jairomelo Jan 5, 2026
df5d080
feat: add ProjectInfo class and project management functions for bett…
jairomelo Jan 5, 2026
58dece1
fix: update focus calibration message for clarity
jairomelo Jan 5, 2026
732313c
feat: implement CameraRegistry for managing camera identification and…
jairomelo Jan 5, 2026
76b7a39
feat: add lens_position attribute to CameraConfig for manual focus co…
jairomelo Jan 5, 2026
7864057
feat: enhance Picamera2 backend with improved configuration managemen…
jairomelo Jan 5, 2026
4d83dfa
feat: add archival metadata extraction and handling in image capture …
jairomelo Jan 5, 2026
facdbf0
feat: support multi-format image capture with JPEG and raw sensor data
jairomelo Jan 5, 2026
54a0e94
feat: add temporal denoise warmup feature and configuration option fo…
jairomelo Jan 5, 2026
3af6be8
feat: update default image size to medium and enhance CameraConfig do…
jairomelo Jan 5, 2026
9bbd391
feat: add performance and speed optimization tests for camera capture
jairomelo Jan 5, 2026
a4127fa
feat: remove outdated speed optimization tests for camera capture
jairomelo Jan 5, 2026
7ddcf4a
fix: add libcap-dev dependency to Dockerfile
jairomelo Jan 5, 2026
5e81721
refactor: remove unused UVICORN and LOG_LEVEL settings from config
jairomelo Jan 5, 2026
ef25da4
refactor: make authentication offline-compatible
jairomelo Jan 5, 2026
8d8f5b2
fix: update database configuration for psycopg3 compatibility
jairomelo Jan 5, 2026
be5b2d2
feat: implement proper migration workflow with comprehensive initial …
jairomelo Jan 5, 2026
fab5e6a
docs: add development guide to prevent common AI-assisted mistakes
jairomelo Jan 5, 2026
5aadf99
feat: add user self-deletion endpoint
jairomelo Jan 5, 2026
9a05225
fix from PR #8 (imports, main func in service.py, lens pos, ...)
Asemica-me Jan 6, 2026
3c7ec32
feat: integrate capture system with API and CRUD operations
Asemica-me Jan 6, 2026
6bfd0f5
feat: wiring API endpoints and capture modules
Asemica-me Jan 9, 2026
4727371
feat: implemented mechanism for image thumbnail generation
Asemica-me Jan 9, 2026
20708a5
fix: missing python-multipart dependency
jairomelo Jan 10, 2026
762f762
Revert config camera default to values tested. Keep comments for docu…
jairomelo Jan 10, 2026
2366c21
refactor: update DATABASE_URL construction and .env file loading
jairomelo Jan 10, 2026
8614051
chore: remove obsolete .env and .env-dist files
jairomelo Jan 11, 2026
8cec4f6
resolved merge conflict
Asemica-me Jan 21, 2026
553b2bc
chore: update .gitignore and add pixi.toml for project configuration
jairomelo Feb 15, 2026
f102d99
fix: update Python version constraint and correct comment in pixi.toml
jairomelo Feb 15, 2026
2c5c6f5
feat: move setup script for DTK capture service dependencies
jairomelo Feb 15, 2026
b26914e
feat: add UserLogin schema and update login endpoint to use it
jairomelo Feb 16, 2026
a387eee
feat: rename documents to records and update related models and endpo…
jairomelo Feb 17, 2026
3cab342
feat: add collections feature with CRUD operations and update related…
jairomelo Feb 17, 2026
328c057
feat: add filtering options for listing records by project and collec…
jairomelo Feb 17, 2026
38a5b5b
feat: refactor Record and RecordImage models, update related schemas …
jairomelo Feb 17, 2026
3ad4181
feat: enhance capture functionality to link or create records and upd…
jairomelo Feb 17, 2026
26d749b
feat: update .gitignore to include backup files
jairomelo Feb 17, 2026
d52bfe0
feat: implement on-demand thumbnail generation for images and update …
jairomelo Feb 17, 2026
8555e23
feat: enhance authentication to support token retrieval from query pa…
jairomelo Feb 17, 2026
475e4fc
feat: update thumbnail generation paths to store alongside source images
jairomelo Feb 17, 2026
3aa23d7
feat: add system router for temperature endpoint and integrate into m…
jairomelo Feb 17, 2026
3ecf756
feat: update record handling in project API to use Record model inste…
jairomelo Feb 18, 2026
7909c22
feat: update record handling in collection deletion to use Record model
jairomelo Feb 18, 2026
8a68499
feat: add orphaned option to list records
jairomelo Feb 18, 2026
e5d26f2
feat: update CORS middleware to allow production Node server
jairomelo Feb 20, 2026
06a38c8
feat: remove redundant FastAPI initialization
jairomelo Feb 21, 2026
34990a7
feat: add operational status check for camera devices
jairomelo Feb 21, 2026
7658691
feat: enhance camera operational check using subprocess for reliability
jairomelo Feb 21, 2026
e626204
feat: remove camera operational probing logic and related imports
jairomelo Feb 21, 2026
a172e0e
feat: update .gitignore to include .DS_Store and retain *.backup
jairomelo Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
.venv
.pixi
pixi.lock
__pycache__
*.db
data
.env
_data/
_logs/
*.pdf
*.pdf
*.backup
.DS_Store
3 changes: 2 additions & 1 deletion alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
from app.core.config import settings

# Import all models so they are registered with Base.metadata
import app.models.document # noqa: F401
import app.models.record # noqa: F401
import app.models.camera # noqa: F401
import app.models.project # noqa: F401
import app.models.collection # noqa: F401
import app.models.user # noqa: F401

# this is the Alembic Config object, which provides
Expand Down
149 changes: 149 additions & 0 deletions alembic/versions/19e2aefe5b17_refactor_separate_record_from_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""refactor: separate Record from RecordImage models

Revision ID: 19e2aefe5b17
Revises: 48189f9482e3
Create Date: 2026-02-16 22:01:11.720584

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = '19e2aefe5b17'
down_revision: Union[str, None] = '48189f9482e3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Step 1: Create the new records table
op.create_table('records',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('object_typology', sa.String(length=50), nullable=True),
sa.Column('author', sa.String(length=255), nullable=True),
sa.Column('material', sa.String(length=255), nullable=True),
sa.Column('date', sa.String(length=50), nullable=True),
sa.Column('custom_attributes', sa.Text(), nullable=True),
sa.Column('project_id', sa.Integer(), nullable=True),
sa.Column('collection_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('modified_at', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=255), nullable=True),
sa.CheckConstraint('NOT (project_id IS NOT NULL AND collection_id IS NOT NULL)', name='check_record_single_parent'),
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_records_id'), 'records', ['id'], unique=False)

# Step 2: Add new columns to record_images (nullable first for data migration)
op.add_column('record_images', sa.Column('record_id', sa.Integer(), nullable=True))
op.add_column('record_images', sa.Column('capture_id', sa.String(length=36), nullable=True))
op.add_column('record_images', sa.Column('pair_id', sa.String(length=36), nullable=True))
op.add_column('record_images', sa.Column('sequence', sa.Integer(), nullable=True))
op.add_column('record_images', sa.Column('role', sa.String(length=50), nullable=True))

# Step 3: Migrate existing data - create one Record per RecordImage
# This preserves all existing descriptive metadata
connection = op.get_bind()

# Get all existing record_images
result = connection.execute(sa.text("""
SELECT id, title, description, object_typology, author, material, date,
custom_attributes, project_id, collection_id, created_at, uploaded_by
FROM record_images
"""))

for row in result:
# Create a Record from each RecordImage's descriptive metadata
record_title = row.title or f"Untitled Record {row.id}"

connection.execute(sa.text("""
INSERT INTO records (title, description, object_typology, author, material, date,
custom_attributes, project_id, collection_id, created_at, created_by)
VALUES (:title, :description, :object_typology, :author, :material, :date,
:custom_attributes, :project_id, :collection_id, :created_at, :created_by)
"""), {
'title': record_title,
'description': row.description,
'object_typology': row.object_typology,
'author': row.author,
'material': row.material,
'date': row.date,
'custom_attributes': row.custom_attributes,
'project_id': row.project_id,
'collection_id': row.collection_id,
'created_at': row.created_at,
'created_by': row.uploaded_by
})

# Get the ID of the just-created record
new_record_id_result = connection.execute(sa.text("SELECT lastval()"))
new_record_id = new_record_id_result.scalar()

# Link the RecordImage to the new Record
connection.execute(sa.text("""
UPDATE record_images SET record_id = :record_id WHERE id = :image_id
"""), {'record_id': new_record_id, 'image_id': row.id})

# Step 4: Now make record_id NOT NULL (all rows should have values now)
op.alter_column('record_images', 'record_id', nullable=False)

# Step 5: Update indexes and constraints
op.drop_index(op.f('ix_record_images_filename'), table_name='record_images')
op.create_index(op.f('ix_record_images_filename'), 'record_images', ['filename'], unique=False)
op.create_index(op.f('ix_record_images_capture_id'), 'record_images', ['capture_id'], unique=False)
op.create_index(op.f('ix_record_images_pair_id'), 'record_images', ['pair_id'], unique=False)
op.create_index(op.f('ix_record_images_record_id'), 'record_images', ['record_id'], unique=False)
op.drop_constraint('record_images_collection_id_fkey', 'record_images', type_='foreignkey')
op.drop_constraint('record_images_project_id_fkey', 'record_images', type_='foreignkey')
op.create_foreign_key(None, 'record_images', 'records', ['record_id'], ['id'], ondelete='CASCADE')

# Step 6: Drop old columns from record_images
op.drop_column('record_images', 'modified_at')
op.drop_column('record_images', 'material')
op.drop_column('record_images', 'collection_id')
op.drop_column('record_images', 'project_id')
Comment on lines +108 to +112
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration drops columns project_id and collection_id from record_images (lines 111-112) without first dropping the check_record_single_parent constraint that was created in the previous migration (48189f9482e3) which references these columns. This could cause the migration to fail. The constraint should be explicitly dropped before dropping the columns it references.

Copilot uses AI. Check for mistakes.
op.drop_column('record_images', 'custom_attributes')
op.drop_column('record_images', 'description')
op.drop_column('record_images', 'object_typology')
op.drop_column('record_images', 'title')
op.drop_column('record_images', 'author')
op.drop_column('record_images', 'date')
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('record_images', sa.Column('date', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('author', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('object_typology', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('custom_attributes', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('project_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('collection_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('material', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('record_images', sa.Column('modified_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.drop_constraint(None, 'record_images', type_='foreignkey')
op.create_foreign_key(op.f('record_images_project_id_fkey'), 'record_images', 'projects', ['project_id'], ['id'], ondelete='SET NULL')
op.create_foreign_key(op.f('record_images_collection_id_fkey'), 'record_images', 'collections', ['collection_id'], ['id'], ondelete='SET NULL')
op.drop_index(op.f('ix_record_images_record_id'), table_name='record_images')
op.drop_index(op.f('ix_record_images_pair_id'), table_name='record_images')
op.drop_index(op.f('ix_record_images_capture_id'), table_name='record_images')
op.drop_index(op.f('ix_record_images_filename'), table_name='record_images')
op.create_index(op.f('ix_record_images_filename'), 'record_images', ['filename'], unique=True)
op.drop_column('record_images', 'role')
op.drop_column('record_images', 'sequence')
op.drop_column('record_images', 'pair_id')
op.drop_column('record_images', 'capture_id')
op.drop_column('record_images', 'record_id')
op.drop_index(op.f('ix_records_id'), table_name='records')
op.drop_table('records')
# ### end Alembic commands ###
59 changes: 59 additions & 0 deletions alembic/versions/48189f9482e3_add_collections_table_and_update_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""add collections table and update records with collection_id

Revision ID: 48189f9482e3
Revises: c3d4e5f6a7b8
Create Date: 2026-02-16 20:22:02.487411

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '48189f9482e3'
down_revision: Union[str, None] = 'c3d4e5f6a7b8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('collections',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('collection_type', sa.String(length=50), nullable=True),
sa.Column('project_id', sa.Integer(), nullable=True),
sa.Column('parent_collection_id', sa.Integer(), nullable=True),
sa.Column('archival_metadata', sa.JSON(), nullable=True),
sa.Column('created_by', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.CheckConstraint('(project_id IS NOT NULL AND parent_collection_id IS NULL) OR (project_id IS NULL AND parent_collection_id IS NOT NULL)', name='check_collection_parent'),
sa.ForeignKeyConstraint(['parent_collection_id'], ['collections.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_collections_id'), 'collections', ['id'], unique=False)
op.create_index(op.f('ix_collections_name'), 'collections', ['name'], unique=False)
op.add_column('record_images', sa.Column('collection_id', sa.Integer(), nullable=True))
op.drop_constraint(op.f('document_images_project_id_fkey'), 'record_images', type_='foreignkey')
op.create_foreign_key(None, 'record_images', 'projects', ['project_id'], ['id'], ondelete='SET NULL')
op.create_foreign_key(None, 'record_images', 'collections', ['collection_id'], ['id'], ondelete='SET NULL')
op.create_check_constraint('check_record_single_parent', 'record_images', 'NOT (project_id IS NOT NULL AND collection_id IS NOT NULL)')
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('check_record_single_parent', 'record_images', type_='check')
op.drop_constraint(None, 'record_images', type_='foreignkey')
op.drop_constraint(None, 'record_images', type_='foreignkey')
op.create_foreign_key(op.f('document_images_project_id_fkey'), 'record_images', 'projects', ['project_id'], ['id'])
op.drop_column('record_images', 'collection_id')
op.drop_index(op.f('ix_collections_name'), table_name='collections')
op.drop_index(op.f('ix_collections_id'), table_name='collections')
op.drop_table('collections')
# ### end Alembic commands ###
87 changes: 87 additions & 0 deletions alembic/versions/c3d4e5f6a7b8_rename_documents_to_records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""rename documents to records

Revision ID: c3d4e5f6a7b8
Revises: b7c8d9e0f1a2
Create Date: 2026-02-16 19:36:06.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'c3d4e5f6a7b8'
down_revision: Union[str, None] = 'b7c8d9e0f1a2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# Rename the document_images table to record_images
op.rename_table('document_images', 'record_images')

# Rename the foreign key column in camera_settings
op.alter_column('camera_settings', 'document_image_id',
new_column_name='record_image_id',
existing_type=sa.Integer(),
existing_nullable=False)

# Rename the foreign key column in exif_data
op.alter_column('exif_data', 'document_image_id',
new_column_name='record_image_id',
existing_type=sa.Integer(),
existing_nullable=False)

# Update the foreign key constraint in camera_settings
# Drop old foreign key constraint
op.drop_constraint('camera_settings_document_image_id_fkey', 'camera_settings', type_='foreignkey')
# Create new foreign key constraint
op.create_foreign_key('camera_settings_record_image_id_fkey',
'camera_settings', 'record_images',
['record_image_id'], ['id'])

# Update the foreign key constraint in exif_data
# Drop old foreign key constraint
op.drop_constraint('exif_data_document_image_id_fkey', 'exif_data', type_='foreignkey')
# Create new foreign key constraint
op.create_foreign_key('exif_data_record_image_id_fkey',
'exif_data', 'record_images',
['record_image_id'], ['id'])

# Rename indexes
op.execute('ALTER INDEX ix_document_images_id RENAME TO ix_record_images_id')
op.execute('ALTER INDEX ix_document_images_filename RENAME TO ix_record_images_filename')


def downgrade() -> None:
# Reverse the index renames
op.execute('ALTER INDEX ix_record_images_filename RENAME TO ix_document_images_filename')
op.execute('ALTER INDEX ix_record_images_id RENAME TO ix_document_images_id')

# Drop the new foreign key constraints
op.drop_constraint('exif_data_record_image_id_fkey', 'exif_data', type_='foreignkey')
op.drop_constraint('camera_settings_record_image_id_fkey', 'camera_settings', type_='foreignkey')

# Recreate the old foreign key constraints
op.create_foreign_key('exif_data_document_image_id_fkey',
'exif_data', 'document_images',
['record_image_id'], ['id'])
op.create_foreign_key('camera_settings_document_image_id_fkey',
'camera_settings', 'document_images',
['record_image_id'], ['id'])

# Rename the foreign key columns back
op.alter_column('exif_data', 'record_image_id',
new_column_name='document_image_id',
existing_type=sa.Integer(),
existing_nullable=False)

op.alter_column('camera_settings', 'record_image_id',
new_column_name='document_image_id',
existing_type=sa.Integer(),
existing_nullable=False)

# Rename the table back
op.rename_table('record_images', 'document_images')
23 changes: 17 additions & 6 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from fastapi import APIRouter, Depends, HTTPException, Security
from fastapi import APIRouter, Depends, HTTPException, Security, Query
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from typing import Optional

from app.api.deps import get_db_dependency
from app.models.user import User
from app.schemas.user import UserCreate, UserRead, PasswordReset, PasswordResetRequest, TokenRefresh
from app.schemas.user import UserCreate, UserLogin, UserRead, PasswordReset, PasswordResetRequest, TokenRefresh
from app.core.security import hash_password, verify_password, create_access_token, verify_access_token

router = APIRouter()
security = HTTPBearer()
# auto_error=False so the dependency doesn't raise when header is absent
# (allows falling back to ?token= query param for browser src= requests)
_optional_bearer = HTTPBearer(auto_error=False)


@router.post("/register", response_model=UserRead)
Expand All @@ -23,7 +27,7 @@ def register(payload: UserCreate, db: Session = Depends(get_db_dependency)):


@router.post("/login")
def login(payload: UserCreate, db: Session = Depends(get_db_dependency)):
def login(payload: UserLogin, db: Session = Depends(get_db_dependency)):
user = db.query(User).filter(User.username == payload.username).first()
if not user or not verify_password(payload.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
Expand Down Expand Up @@ -72,9 +76,16 @@ def reset_password(
return {"detail": "password updated successfully"}


def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security), db: Session = Depends(get_db_dependency)):
token = credentials.credentials
payload = verify_access_token(token)
def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(_optional_bearer),
token: Optional[str] = Query(default=None),
db: Session = Depends(get_db_dependency)
):
# Accept token from Authorization header OR ?token= query param (needed for <img src>)
raw_token = credentials.credentials if credentials else token
if not raw_token:
raise HTTPException(status_code=401, detail="Not authenticated")
payload = verify_access_token(raw_token)
Comment on lines +84 to +88
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting authentication tokens via query parameters poses a security risk. Query parameters are often logged in server logs, browser history, and referrer headers, potentially exposing sensitive tokens. While this may be necessary for browser image src attributes, consider implementing short-lived temporary tokens specifically for image access, or use a separate endpoint that validates the token and redirects to a signed URL.

Copilot uses AI. Check for mistakes.
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
user_id = int(payload.get("sub"))
Expand Down
Loading