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:
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user