d582f15681
- 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.
92 lines
3.1 KiB
Python
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}
|