from typing import Annotated from fastapi import APIRouter, Header from pydantic import BaseModel, EmailStr, Field from app.dependencies import Context, Csrf, CurrentSession, Db from app.errors import bad_request, forbidden from app.exports import service as exports_service from app.identity import service as identity_service from app.identity.mailer import send_token_email from app.security import constant_time_equals from app.spaces.service import list_spaces_for_user, require_owner router = APIRouter(prefix="/spaces/current", tags=["spaces"]) class DeletionBody(BaseModel): confirmation_phrase: str = Field(alias="confirmationPhrase") class MemberInviteBody(BaseModel): email: EmailStr @router.get("") def current(context: Context) -> dict: return { "id": context.space.id, "name": context.space.name, "status": context.space.status, "role": context.membership.role, } @router.post("/invitations", status_code=201) def invite_member(body: MemberInviteBody, db: Db, context: Context, _csrf: Csrf) -> dict: require_owner(context.membership) invitation, token = identity_service.create_invitation( db, email=str(body.email), invited_by_user_id=context.user.id, space_id=context.space.id, ) db.commit() send_token_email(recipient=invitation.email, token=token, kind="invite") return {"id": invitation.id, "expiresAt": invitation.expires_at} @router.post("/deletion", status_code=202) def schedule_deletion(body: DeletionBody, db: Db, context: Context, _csrf: Csrf) -> dict: if body.confirmation_phrase != "DELETE": raise bad_request("SPACE_DELETION_CONFIRMATION_INVALID", "Confirmation phrase is invalid") require_owner(context.membership) scheduled_at = exports_service.schedule_deletion(db, space=context.space) return { "status": "pending_deletion", "scheduledAt": scheduled_at, "cancelsUntil": scheduled_at, } @router.get("/deletion") def deletion_status(db: Db, auth: CurrentSession) -> dict: user, _ = auth spaces = list_spaces_for_user(db, user=user) if not spaces: raise forbidden("SPACE_MEMBERSHIP_REQUIRED", "No financial space is available") space, _membership = spaces[0] return { "status": "pending_deletion" if space.status == "blocked" else "active", "scheduledAt": space.deletion_scheduled_at, "cancelsUntil": space.deletion_scheduled_at, } @router.delete("/deletion") def cancel_deletion( db: Db, auth: CurrentSession, csrf_header: Annotated[str | None, Header(alias="X-CSRF-Token")] = None, ) -> dict: user, session = auth if not csrf_header or not constant_time_equals(session.csrf_token, csrf_header): raise forbidden("IDENTITY_CSRF_INVALID", "CSRF token is missing or invalid") spaces = list_spaces_for_user(db, user=user) if not spaces: raise forbidden("SPACE_MEMBERSHIP_REQUIRED", "No financial space is available") space, membership = spaces[0] require_owner(membership) exports_service.cancel_deletion(db, space=space) return {"status": "active", "scheduledAt": None, "cancelsUntil": None}