feat: complete user config management

- Add theme support with dark/light/system modes
- Add useTheme hook for applying user config theme
- Update router to use SettingsPage
- Update app-shell to apply theme on load
- Add CSS variables for dark theme
- Fix mypy errors in user_config.py
- Quality gates pass: ruff, mypy, typecheck, lint, build
This commit is contained in:
Fusion
2026-05-18 15:58:01 +02:00
parent 9f7a750898
commit 94254ee3fd
18 changed files with 463 additions and 171 deletions
@@ -0,0 +1,35 @@
"""add user_configs table
Revision ID: 0003
Revises: 0002
Create Date: 2025-05-18
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0003'
down_revision: Union[str, None] = '0002'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
'user_configs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('user_id', sa.UUID(), nullable=False),
sa.Column('config', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
def downgrade() -> None:
op.drop_table('user_configs')
+96
View File
@@ -0,0 +1,96 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Cookie, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Any
from src.auth.jwt_service import decode_access_token
from src.config import Settings
from src.database import SessionLocal
from src.models.user import User
from src.models.user_config import UserConfig
router = APIRouter(prefix="/users/me", tags=["user-config"])
async def get_db_session():
async with SessionLocal() as session:
yield session
async def get_current_user_id(
access_token: Annotated[str | None, Cookie()] = None,
) -> uuid.UUID:
if not access_token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing access token")
try:
claims = decode_access_token(settings=Settings(), token=access_token)
return uuid.UUID(str(claims["sub"]))
except Exception:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid access token")
async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
user = await session.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found")
return user
async def _get_or_create_config(session: AsyncSession, user_id: uuid.UUID) -> UserConfig:
result = await session.execute(select(UserConfig).where(UserConfig.user_id == user_id))
config = result.scalar_one_or_none()
if config is None:
config = UserConfig(user_id=user_id, config={})
session.add(config)
await session.commit()
await session.refresh(config)
return config
class UserConfigResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
default_editor: str | None = None
theme: str = "system"
git_user_name: str | None = None
git_user_email: str | None = None
class UserConfigUpdate(BaseModel):
default_editor: str | None = None
theme: str | None = None
git_user_name: str | None = None
git_user_email: str | None = None
@router.get("/config", response_model=UserConfigResponse)
async def get_user_config(
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> UserConfigResponse:
_user = await _get_user(session, user_id)
config = await _get_or_create_config(session, user_id)
return UserConfigResponse.model_validate(config.config)
@router.patch("/config", response_model=UserConfigResponse)
async def update_user_config(
data: UserConfigUpdate,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> UserConfigResponse:
_user = await _get_user(session, user_id)
config = await _get_or_create_config(session, user_id)
# Merge updates
update_data = data.model_dump(exclude_unset=True, exclude_none=True)
config.config.update(update_data)
await session.commit()
await session.refresh(config)
return UserConfigResponse.model_validate(config.config)
+2
View File
@@ -5,6 +5,7 @@ from src.api.auth import router as auth_router
from src.api.git_repositories import router as git_repositories_router
from src.api.projects import router as projects_router
from src.api.ssh_keys import router as ssh_keys_router
from src.api.user_config import router as user_config_router
from src.api.users import router as users_router
app = FastAPI(title="Headquarter API")
@@ -13,4 +14,5 @@ app.include_router(projects_router)
app.include_router(users_router)
app.include_router(ssh_keys_router)
app.include_router(git_repositories_router)
app.include_router(user_config_router)
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")