"""HTTP surface for security tips. Two router prefixes mounted from a single module so the auth gates are explicit at the route layer: * ``/api/v1/security-tips`` — read-only, ANY authenticated user. Backs the header banner (``GET``). * ``/api/v1/admin/security-tips`` — full CRUD + reorder, restricted to ``global_admin``. The management role (``management``) is READ-ONLY platform-wide and deliberately excluded — the brief is "leadership view, no operational changes". """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, Request, status from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthUser from app.core.database import get_db from app.core.dependencies import get_current_user, require_admin, require_config_read from app.core.response import success_response from app.domains.platform_tips.schemas import ( SecurityTipAdmin, SecurityTipCreate, SecurityTipPublic, SecurityTipReorder, SecurityTipUpdate, ) from app.domains.platform_tips.service import PlatformTipsService # ── Public read ───────────────────────────────────────────────────── public_router = APIRouter( prefix="/api/v1/security-tips", tags=["security-tips"], ) @public_router.get("") async def list_public( _: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Return the enabled tips, ordered. Available to any signed-in user — the header banner needs to render for everyone, including auditors / hr / finance / management.""" svc = PlatformTipsService(db) rows = await svc.list_visible() return success_response( data=[SecurityTipPublic.model_validate(r).model_dump(mode="json") for r in rows], ) # ── Admin CRUD ────────────────────────────────────────────────────── admin_router = APIRouter( prefix="/api/v1/admin/security-tips", tags=["security-tips-admin"], ) @admin_router.get("") async def list_admin( _: AuthUser = Depends(require_config_read()), db: AsyncSession = Depends(get_db), ): svc = PlatformTipsService(db) rows = await svc.list_all() return success_response( data=[SecurityTipAdmin.model_validate(r).model_dump(mode="json") for r in rows], ) @admin_router.post("", status_code=status.HTTP_201_CREATED) async def create_admin( body: SecurityTipCreate, request: Request, user: AuthUser = Depends(require_admin()), db: AsyncSession = Depends(get_db), ): svc = PlatformTipsService(db) row = await svc.create( body, actor_id=user.id, trace_id=getattr(request.state, "trace_id", ""), ) return success_response( data=SecurityTipAdmin.model_validate(row).model_dump(mode="json"), ) @admin_router.put("/{tip_id}") async def update_admin( tip_id: uuid.UUID, body: SecurityTipUpdate, request: Request, user: AuthUser = Depends(require_admin()), db: AsyncSession = Depends(get_db), ): svc = PlatformTipsService(db) row = await svc.update( tip_id, body, actor_id=user.id, trace_id=getattr(request.state, "trace_id", ""), ) return success_response( data=SecurityTipAdmin.model_validate(row).model_dump(mode="json"), ) @admin_router.delete("/{tip_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_admin( tip_id: uuid.UUID, request: Request, user: AuthUser = Depends(require_admin()), db: AsyncSession = Depends(get_db), ): svc = PlatformTipsService(db) await svc.delete( tip_id, actor_id=user.id, trace_id=getattr(request.state, "trace_id", ""), ) return None @admin_router.post("/reorder") async def reorder_admin( body: SecurityTipReorder, request: Request, user: AuthUser = Depends(require_admin()), db: AsyncSession = Depends(get_db), ): svc = PlatformTipsService(db) rows = await svc.reorder( body.ordered_ids, actor_id=user.id, trace_id=getattr(request.state, "trace_id", ""), ) return success_response( data=[SecurityTipAdmin.model_validate(r).model_dump(mode="json") for r in rows], )