Files
finance/backend/app/spaces/router.py
T
k1nq d582f15681 feat: add tests for import polling, login page, money utilities, and query keys
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
2026-09-14 09:26:33 +05:00

92 lines
3.1 KiB
Python

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}