48 lines
1.6 KiB
Python
48 lines
1.6 KiB
Python
"""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)
|