Files
TJWaterServerBinary/app/infra/repositories/metadata_repository.py

198 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from dataclasses import dataclass
from typing import Optional, List
from uuid import UUID
from cryptography.fernet import InvalidToken
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.encryption import (
get_database_encryptor,
get_encryptor,
is_database_encryption_configured,
is_encryption_configured,
)
from app.infra.db.metadata import models
def _normalize_postgres_dsn(dsn: str) -> str:
if not dsn or "://" not in dsn:
return dsn
scheme, rest = dsn.split("://", 1)
if scheme not in ("postgresql", "postgres", "postgresql+psycopg"):
return dsn
if "@" not in rest:
return dsn
userinfo, hostinfo = rest.rsplit("@", 1)
if ":" not in userinfo:
return dsn
username, password = userinfo.split(":", 1)
if "@" not in password:
return dsn
password = password.replace("@", "%40")
return f"{scheme}://{username}:{password}@{hostinfo}"
@dataclass(frozen=True)
class ProjectDbRouting:
project_id: UUID
db_role: str
db_type: str
dsn: str
pool_min_size: int
pool_max_size: int
@dataclass(frozen=True)
class ProjectGeoServerInfo:
project_id: UUID
gs_base_url: Optional[str]
gs_admin_user: Optional[str]
gs_admin_password: Optional[str]
gs_datastore_name: str
default_extent: Optional[dict]
srid: int
@dataclass(frozen=True)
class ProjectSummary:
project_id: UUID
name: str
code: str
description: Optional[str]
gs_workspace: str
status: str
project_role: str
class MetadataRepository:
"""元数据访问层system_hub"""
def __init__(self, session: AsyncSession):
self.session = session
async def get_user_by_keycloak_id(self, keycloak_id: UUID) -> Optional[models.User]:
result = await self.session.execute(
select(models.User).where(models.User.keycloak_id == keycloak_id)
)
return result.scalar_one_or_none()
async def get_project_by_id(self, project_id: UUID) -> Optional[models.Project]:
result = await self.session.execute(
select(models.Project).where(models.Project.id == project_id)
)
return result.scalar_one_or_none()
async def get_membership_role(
self, project_id: UUID, user_id: UUID
) -> Optional[str]:
result = await self.session.execute(
select(models.UserProjectMembership.project_role).where(
models.UserProjectMembership.project_id == project_id,
models.UserProjectMembership.user_id == user_id,
)
)
return result.scalar_one_or_none()
async def get_project_db_routing(
self, project_id: UUID, db_role: str
) -> Optional[ProjectDbRouting]:
result = await self.session.execute(
select(models.ProjectDatabase).where(
models.ProjectDatabase.project_id == project_id,
models.ProjectDatabase.db_role == db_role,
)
)
record = result.scalar_one_or_none()
if not record:
return None
if not is_database_encryption_configured():
raise ValueError("DATABASE_ENCRYPTION_KEY is not configured")
encryptor = get_database_encryptor()
try:
dsn = encryptor.decrypt(record.dsn_encrypted)
except InvalidToken:
raise ValueError(
"Failed to decrypt project DB DSN: DATABASE_ENCRYPTION_KEY mismatch "
"or invalid dsn_encrypted value"
)
dsn = _normalize_postgres_dsn(dsn)
return ProjectDbRouting(
project_id=record.project_id,
db_role=record.db_role,
db_type=record.db_type,
dsn=dsn,
pool_min_size=record.pool_min_size,
pool_max_size=record.pool_max_size,
)
async def get_geoserver_config(
self, project_id: UUID
) -> Optional[ProjectGeoServerInfo]:
result = await self.session.execute(
select(models.ProjectGeoServerConfig).where(
models.ProjectGeoServerConfig.project_id == project_id
)
)
record = result.scalar_one_or_none()
if not record:
return None
if record.gs_admin_password_encrypted:
if is_encryption_configured():
encryptor = get_encryptor()
password = encryptor.decrypt(record.gs_admin_password_encrypted)
else:
password = record.gs_admin_password_encrypted
else:
password = None
return ProjectGeoServerInfo(
project_id=record.project_id,
gs_base_url=record.gs_base_url,
gs_admin_user=record.gs_admin_user,
gs_admin_password=password,
gs_datastore_name=record.gs_datastore_name,
default_extent=record.default_extent,
srid=record.srid,
)
async def list_projects_for_user(self, user_id: UUID) -> List[ProjectSummary]:
stmt = (
select(models.Project, models.UserProjectMembership.project_role)
.join(
models.UserProjectMembership,
models.UserProjectMembership.project_id == models.Project.id,
)
.where(models.UserProjectMembership.user_id == user_id)
.order_by(models.Project.name)
)
result = await self.session.execute(stmt)
return [
ProjectSummary(
project_id=project.id,
name=project.name,
code=project.code,
description=project.description,
gs_workspace=project.gs_workspace,
status=project.status,
project_role=role,
)
for project, role in result.all()
]
async def list_all_projects(self) -> List[ProjectSummary]:
result = await self.session.execute(
select(models.Project).order_by(models.Project.name)
)
return [
ProjectSummary(
project_id=project.id,
name=project.name,
code=project.code,
description=project.description,
gs_workspace=project.gs_workspace,
status=project.status,
project_role="owner",
)
for project in result.scalars().all()
]