From 6fc3aa52098bcfdeca9d99ef396163162c537d05 Mon Sep 17 00:00:00 2001 From: Jiang Date: Tue, 24 Feb 2026 17:02:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=92=8C=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/meta.py | 16 +++- app/auth/metadata_dependencies.py | 16 +++- app/auth/project_dependencies.py | 118 +++++++++++++++++++++--------- 3 files changed, 112 insertions(+), 38 deletions(-) diff --git a/app/api/v1/endpoints/meta.py b/app/api/v1/endpoints/meta.py index 098b608..1e86a44 100644 --- a/app/api/v1/endpoints/meta.py +++ b/app/api/v1/endpoints/meta.py @@ -1,6 +1,8 @@ +import logging from fastapi import APIRouter, Depends, HTTPException, status from psycopg import AsyncConnection from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.auth.project_dependencies import ( @@ -20,6 +22,7 @@ from app.domain.schemas.metadata import ( from app.infra.repositories.metadata_repository import MetadataRepository router = APIRouter() +logger = logging.getLogger(__name__) @router.get("/meta/project", response_model=ProjectMetaResponse) @@ -61,7 +64,18 @@ async def list_user_projects( current_user=Depends(get_current_metadata_user), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ): - projects = await metadata_repo.list_projects_for_user(current_user.id) + try: + projects = await metadata_repo.list_projects_for_user(current_user.id) + except SQLAlchemyError as exc: + logger.error( + "Metadata DB error while listing projects for user %s", + current_user.id, + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Metadata database error: {exc}", + ) from exc return [ ProjectSummaryResponse( project_id=project.project_id, diff --git a/app/auth/metadata_dependencies.py b/app/auth/metadata_dependencies.py index 323d549..7846396 100644 --- a/app/auth/metadata_dependencies.py +++ b/app/auth/metadata_dependencies.py @@ -1,7 +1,9 @@ from dataclasses import dataclass from uuid import UUID +import logging from fastapi import Depends, HTTPException, status +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.auth.keycloak_dependencies import get_current_keycloak_sub @@ -9,6 +11,8 @@ from app.core.config import settings from app.infra.db.metadata.database import get_metadata_session from app.infra.repositories.metadata_repository import MetadataRepository +logger = logging.getLogger(__name__) + async def get_metadata_repository( session: AsyncSession = Depends(get_metadata_session), @@ -20,7 +24,17 @@ async def get_current_metadata_user( keycloak_sub: UUID = Depends(get_current_keycloak_sub), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ): - user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub) + try: + user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub) + except SQLAlchemyError as exc: + logger.error( + "Metadata DB error while resolving current user", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Metadata database error: {exc}", + ) from exc if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user" diff --git a/app/auth/project_dependencies.py b/app/auth/project_dependencies.py index f9dee9d..ba2c172 100644 --- a/app/auth/project_dependencies.py +++ b/app/auth/project_dependencies.py @@ -2,8 +2,10 @@ from dataclasses import dataclass from typing import AsyncGenerator from uuid import UUID +import logging from fastapi import Depends, Header, HTTPException, status from psycopg import AsyncConnection +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.auth.keycloak_dependencies import get_current_keycloak_sub @@ -12,11 +14,13 @@ from app.infra.db.dynamic_manager import project_connection_manager from app.infra.db.metadata.database import get_metadata_session from app.infra.repositories.metadata_repository import MetadataRepository -DB_ROLE_BIZ_PG = "biz_pg" -DB_ROLE_IOT_TS = "iot_ts" +DB_ROLE_BIZ_DATA = "biz_data" +DB_ROLE_IOT_DATA = "iot_data" DB_TYPE_POSTGRES = "postgresql" DB_TYPE_TIMESCALE = "timescaledb" +logger = logging.getLogger(__name__) + @dataclass(frozen=True) class ProjectContext: @@ -43,31 +47,43 @@ async def get_project_context( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid project id" ) from exc - project = await metadata_repo.get_project_by_id(project_uuid) - if not project: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" - ) - if project.status != "active": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active" - ) + try: + project = await metadata_repo.get_project_by_id(project_uuid) + if not project: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) + if project.status != "active": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active" + ) - user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub) - if not user: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="User not registered" - ) - if not user.is_active: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user" - ) + user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub) + if not user: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="User not registered" + ) + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user" + ) - membership_role = await metadata_repo.get_membership_role(project_uuid, user.id) - if not membership_role: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="No access to project" + membership_role = await metadata_repo.get_membership_role( + project_uuid, user.id ) + if not membership_role: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="No access to project" + ) + except SQLAlchemyError as exc: + logger.error( + "Metadata DB error while resolving project context", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Metadata database error: {exc}", + ) from exc return ProjectContext( project_id=project.id, @@ -80,9 +96,19 @@ async def get_project_pg_session( ctx: ProjectContext = Depends(get_project_context), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ) -> AsyncGenerator[AsyncSession, None]: - routing = await metadata_repo.get_project_db_routing( - ctx.project_id, DB_ROLE_BIZ_PG - ) + try: + routing = await metadata_repo.get_project_db_routing( + ctx.project_id, DB_ROLE_BIZ_DATA + ) + except ValueError as exc: + logger.error( + "Missing ENCRYPTION_KEY while resolving project PostgreSQL routing", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="ENCRYPTION_KEY is not configured for project PostgreSQL access", + ) from exc if not routing: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, @@ -98,7 +124,7 @@ async def get_project_pg_session( pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE sessionmaker = await project_connection_manager.get_pg_sessionmaker( ctx.project_id, - DB_ROLE_BIZ_PG, + DB_ROLE_BIZ_DATA, routing.dsn, pool_min_size, pool_max_size, @@ -111,9 +137,19 @@ async def get_project_pg_connection( ctx: ProjectContext = Depends(get_project_context), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ) -> AsyncGenerator[AsyncConnection, None]: - routing = await metadata_repo.get_project_db_routing( - ctx.project_id, DB_ROLE_BIZ_PG - ) + try: + routing = await metadata_repo.get_project_db_routing( + ctx.project_id, DB_ROLE_BIZ_DATA + ) + except ValueError as exc: + logger.error( + "Missing ENCRYPTION_KEY while resolving project PostgreSQL routing", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="ENCRYPTION_KEY is not configured for project PostgreSQL access", + ) from exc if not routing: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, @@ -129,7 +165,7 @@ async def get_project_pg_connection( pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE pool = await project_connection_manager.get_pg_pool( ctx.project_id, - DB_ROLE_BIZ_PG, + DB_ROLE_BIZ_DATA, routing.dsn, pool_min_size, pool_max_size, @@ -142,9 +178,19 @@ async def get_project_timescale_connection( ctx: ProjectContext = Depends(get_project_context), metadata_repo: MetadataRepository = Depends(get_metadata_repository), ) -> AsyncGenerator[AsyncConnection, None]: - routing = await metadata_repo.get_project_db_routing( - ctx.project_id, DB_ROLE_IOT_TS - ) + try: + routing = await metadata_repo.get_project_db_routing( + ctx.project_id, DB_ROLE_IOT_DATA + ) + except ValueError as exc: + logger.error( + "Missing ENCRYPTION_KEY while resolving project TimescaleDB routing", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="ENCRYPTION_KEY is not configured for project TimescaleDB access", + ) from exc if not routing: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, @@ -160,7 +206,7 @@ async def get_project_timescale_connection( pool_max_size = routing.pool_max_size or settings.PROJECT_TS_POOL_MAX_SIZE pool = await project_connection_manager.get_timescale_pool( ctx.project_id, - DB_ROLE_IOT_TS, + DB_ROLE_IOT_DATA, routing.dsn, pool_min_size, pool_max_size,