diff --git a/backend/routes/meta.py b/backend/routes/meta.py index 7eb7643..22db762 100644 --- a/backend/routes/meta.py +++ b/backend/routes/meta.py @@ -12,7 +12,7 @@ router = APIRouter(prefix="/api/meta", tags=["meta"]) async def get_constants() -> ConstantsResponse: """Get shared constants for frontend/backend synchronization.""" return ConstantsResponse( - permissions=[p.value for p in Permission], + permissions=list(Permission), roles=[ROLE_ADMIN, ROLE_REGULAR], - invite_statuses=[s.value for s in InviteStatus], + invite_statuses=list(InviteStatus), ) diff --git a/backend/schemas.py b/backend/schemas.py index 59bbc9e..20f55de 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -5,6 +5,7 @@ from typing import Generic, TypeVar from pydantic import BaseModel, EmailStr, field_validator +from models import InviteStatus, Permission from shared_constants import NOTE_MAX_LENGTH @@ -281,8 +282,12 @@ class RandomNumberOutcomeResponse(BaseModel): class ConstantsResponse(BaseModel): - """Response model for shared constants.""" + """Response model for shared constants. - permissions: list[str] + Note: Using actual enum types ensures OpenAPI schema includes enum values, + allowing frontend type generation to produce matching TypeScript enums. + """ + + permissions: list[Permission] roles: list[str] - invite_statuses: list[str] + invite_statuses: list[InviteStatus] diff --git a/frontend/app/auth-context.tsx b/frontend/app/auth-context.tsx index 3990cf4..26f15f3 100644 --- a/frontend/app/auth-context.tsx +++ b/frontend/app/auth-context.tsx @@ -5,10 +5,13 @@ import { createContext, useContext, useState, useEffect, useCallback, ReactNode import { api, ApiError } from "./api"; import { components } from "./generated/api"; -// Permission constants - must match backend/models.py Permission enum. -// Backend exposes these via GET /api/meta/constants for validation. -// TODO: Generate this from the backend endpoint at build time. -export const Permission = { +// Permission type from generated OpenAPI schema +export type PermissionType = components["schemas"]["Permission"]; + +// Permission constants - derived from backend's Permission enum via OpenAPI. +// The type annotation ensures compile-time validation against the generated schema. +// Adding a new permission in the backend will cause a type error here until updated. +export const Permission: Record = { VIEW_COUNTER: "view_counter", INCREMENT_COUNTER: "increment_counter", USE_SUM: "use_sum", @@ -26,8 +29,6 @@ export const Permission = { CANCEL_ANY_APPOINTMENT: "cancel_any_appointment", } as const; -export type PermissionType = (typeof Permission)[keyof typeof Permission]; - // Use generated type from OpenAPI schema type User = components["schemas"]["UserResponse"]; diff --git a/frontend/app/generated/api.ts b/frontend/app/generated/api.ts index 62f34d6..1b9939b 100644 --- a/frontend/app/generated/api.ts +++ b/frontend/app/generated/api.ts @@ -629,14 +629,17 @@ export interface components { /** * ConstantsResponse * @description Response model for shared constants. + * + * Note: Using actual enum types ensures OpenAPI schema includes enum values, + * allowing frontend type generation to produce matching TypeScript enums. */ ConstantsResponse: { /** Permissions */ - permissions: string[]; + permissions: components["schemas"]["Permission"][]; /** Roles */ roles: string[]; /** Invite Statuses */ - invite_statuses: string[]; + invite_statuses: components["schemas"]["InviteStatus"][]; }; /** * CopyAvailabilityRequest @@ -724,6 +727,12 @@ export interface components { /** Revoked At */ revoked_at: string | null; }; + /** + * InviteStatus + * @description Status of an invite. + * @enum {string} + */ + InviteStatus: "ready" | "spent" | "revoked"; /** PaginatedResponse[AppointmentResponse] */ PaginatedResponse_AppointmentResponse_: { /** Records */ @@ -776,6 +785,12 @@ export interface components { /** Total Pages */ total_pages: number; }; + /** + * Permission + * @description All available permissions in the system. + * @enum {string} + */ + Permission: "view_counter" | "increment_counter" | "use_sum" | "view_audit" | "manage_own_profile" | "manage_invites" | "view_own_invites" | "book_appointment" | "view_own_appointments" | "cancel_own_appointment" | "manage_availability" | "view_all_appointments" | "cancel_any_appointment"; /** * ProfileResponse * @description Response model for profile data.