"""Identity: who am I, and admin-driven member onboarding.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..auth import Ctx, issue_token from ..db import record_audit from ..deps import get_ctx from ..util import USERNAME_RE, now_iso router = APIRouter() class UserCreate(BaseModel): username: str role: str = "member" @router.post("/v1/users") def create_user(body: UserCreate, request: Request, ctx: Ctx = Depends(get_ctx)): """Admin onboards a member directly, bypassing the invite code.""" admin = ctx.require_admin(request) if body.role not in ("admin", "member"): raise HTTPException(400, "role must be 'admin' or 'member'") if not USERNAME_RE.match(body.username or ""): raise HTTPException(400, "invalid username") if ctx.conn.execute( "SELECT 1 FROM users WHERE username = %s", (body.username,) ).fetchone(): raise HTTPException(409, "username already exists") cur = ctx.conn.execute( "INSERT INTO users (username, role, created_at) VALUES (%s, %s, %s) RETURNING id", (body.username, body.role, now_iso()), ) user_id = cur.fetchone()["id"] raw = issue_token(ctx.conn, user_id, "initial") record_audit(ctx.conn, admin["id"], "user.create", {"username": body.username, "role": body.role}) return { "token": raw, "user": {"id": user_id, "username": body.username, "role": body.role}, } @router.get("/v1/me") def me(request: Request, ctx: Ctx = Depends(get_ctx)): return ctx.require_user(request)