feat: Add Approve Identified page and API for pending identifications

This commit introduces a new Approve Identified page in the frontend, allowing users to view and manage pending identifications. The page fetches data from a new API endpoint that lists pending identifications from the auth database. Additionally, the necessary API routes and database session management for handling pending identifications have been implemented. The Layout component has been updated to include navigation to the new page, enhancing the user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-11-18 15:14:16 -05:00
parent 7f48d48b80
commit 1d8ca7e592
8 changed files with 341 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Pending identifications endpoints for approval workflow."""
from __future__ import annotations
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict
from sqlalchemy import text
from sqlalchemy.orm import Session
from src.web.db.session import get_auth_db
router = APIRouter(prefix="/pending-identifications", tags=["pending-identifications"])
class PendingIdentificationResponse(BaseModel):
"""Pending identification DTO returned from API."""
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
id: int
face_id: int
user_id: int
user_name: Optional[str] = None
user_email: str
first_name: str
last_name: str
middle_name: Optional[str] = None
maiden_name: Optional[str] = None
date_of_birth: Optional[date] = None
status: str
created_at: str
updated_at: str
class PendingIdentificationsListResponse(BaseModel):
"""List of pending identifications."""
model_config = ConfigDict(protected_namespaces=())
items: list[PendingIdentificationResponse]
total: int
@router.get("", response_model=PendingIdentificationsListResponse)
def list_pending_identifications(
db: Session = Depends(get_auth_db),
) -> PendingIdentificationsListResponse:
"""List all pending identifications from the auth database.
This endpoint reads from the separate auth database (DATABASE_URL_AUTH)
and returns all pending identifications from the pending_identifications table.
Only shows records with status='pending' for approval.
"""
try:
# Query pending_identifications from auth database using raw SQL
# Join with users table to get user name/email
# Filter by status='pending' to show only records awaiting approval
result = db.execute(text("""
SELECT
pi.id,
pi.face_id,
pi.user_id,
u.name as user_name,
u.email as user_email,
pi.first_name,
pi.last_name,
pi.middle_name,
pi.maiden_name,
pi.date_of_birth,
pi.status,
pi.created_at,
pi.updated_at
FROM pending_identifications pi
LEFT JOIN users u ON pi.user_id = u.id
WHERE pi.status = 'pending'
ORDER BY pi.last_name ASC, pi.first_name ASC, pi.created_at DESC
"""))
rows = result.fetchall()
items = []
for row in rows:
items.append(PendingIdentificationResponse(
id=row.id,
face_id=row.face_id,
user_id=row.user_id,
user_name=row.user_name,
user_email=row.user_email,
first_name=row.first_name,
last_name=row.last_name,
middle_name=row.middle_name,
maiden_name=row.maiden_name,
date_of_birth=row.date_of_birth,
status=row.status,
created_at=str(row.created_at) if row.created_at else '',
updated_at=str(row.updated_at) if row.updated_at else '',
))
return PendingIdentificationsListResponse(items=items, total=len(items))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error reading from auth database: {str(e)}"
)
+2
View File
@@ -15,6 +15,7 @@ from src.web.api.health import router as health_router
from src.web.api.jobs import router as jobs_router
from src.web.api.metrics import router as metrics_router
from src.web.api.people import router as people_router
from src.web.api.pending_identifications import router as pending_identifications_router
from src.web.api.photos import router as photos_router
from src.web.api.tags import router as tags_router
from src.web.api.version import router as version_router
@@ -151,6 +152,7 @@ def create_app() -> FastAPI:
app.include_router(photos_router, prefix="/api/v1")
app.include_router(faces_router, prefix="/api/v1")
app.include_router(people_router, prefix="/api/v1")
app.include_router(pending_identifications_router, prefix="/api/v1")
app.include_router(tags_router, prefix="/api/v1")
return app
+48
View File
@@ -23,6 +23,15 @@ def get_database_url() -> str:
return "sqlite:///data/punimtag.db"
def get_auth_database_url() -> str:
"""Fetch auth database URL from environment."""
import os
db_url = os.getenv("DATABASE_URL_AUTH")
if not db_url:
raise ValueError("DATABASE_URL_AUTH environment variable not set")
return db_url
database_url = get_database_url()
# SQLite-specific configuration
connect_args = {}
@@ -56,3 +65,42 @@ def get_db() -> Generator:
db.close()
# Auth database setup
try:
auth_database_url = get_auth_database_url()
auth_connect_args = {}
if auth_database_url.startswith("sqlite"):
auth_connect_args = {"check_same_thread": False}
auth_pool_kwargs = {"pool_pre_ping": True}
if auth_database_url.startswith("postgresql"):
auth_pool_kwargs.update({
"pool_size": 10,
"max_overflow": 20,
"pool_recycle": 3600,
})
auth_engine = create_engine(
auth_database_url,
future=True,
connect_args=auth_connect_args,
**auth_pool_kwargs
)
AuthSessionLocal = sessionmaker(bind=auth_engine, autoflush=False, autocommit=False, future=True)
except ValueError:
# DATABASE_URL_AUTH not set - auth database not available
auth_engine = None
AuthSessionLocal = None
def get_auth_db() -> Generator:
"""Yield a DB session for auth database request lifecycle."""
if AuthSessionLocal is None:
raise ValueError("Auth database not configured. Set DATABASE_URL_AUTH environment variable.")
db = AuthSessionLocal()
try:
yield db
finally:
db.close()