Phase 10 — Admin Role & Settings (ADMIN-01/02/03) #17

Merged
luckberg merged 25 commits from gsd/phase-10-admin-role-settings into main 2026-06-13 17:10:47 -04:00
Showing only changes of commit bfe1eff5a3 - Show all commits
+136
View File
@@ -63,6 +63,8 @@ export interface MeUser {
id: number; id: number;
displayName: string | null; displayName: string | null;
color: string; color: string;
isAdmin: boolean; // from users.is_admin — UX gating only (D-03); server enforces 403 on /api/admin/*
needsProviderSetup: boolean; // true when no member_credentials row exists for this user
} }
export interface MeResponse { export interface MeResponse {
@@ -325,3 +327,137 @@ export async function fetchWritableCalendars(): Promise<WritableCalendar[]> {
const body = (await res.json()) as { calendars: WritableCalendar[] }; const body = (await res.json()) as { calendars: WritableCalendar[] };
return body.calendars; return body.calendars;
} }
// ── /api/admin/* (Phase 10, Plan 04) ─────────────────────────────────────────
/**
* A member row as returned by GET /api/admin/members.
* Matches the Plan-03 shape: { id, displayName, color, hasCredential }.
*/
export interface AdminMember {
id: number;
displayName: string | null;
color: string;
hasCredential: boolean;
}
export interface AdminMembersResponse {
members: AdminMember[];
}
/**
* Payload for POST /api/admin/credentials (admin-managed rotation).
* NOTE: includes userId (the member being rotated) — admin-scoped.
* The app password MUST NOT be logged or stored beyond the in-flight request body (T-10-15).
*/
export interface SaveCredentialPayload {
userId: number;
providerType: 'caldav';
fastmailEmail: string;
appPassword: string;
}
/**
* A synced calendar row as returned by GET /api/admin/calendars.
* Matches the Plan-03 shape: { id, displayName, isShared }.
*/
export interface AdminCalendar {
id: number;
displayName: string;
isShared: boolean;
}
export interface AdminCalendarsResponse {
calendars: AdminCalendar[];
}
/**
* Payload for POST /api/me/credential (self-service, member-scoped).
* NOTE: no userId field — the server resolves userId from the session (Pitfall 6 / T-10-12).
* The app password MUST NOT be logged or stored beyond the in-flight request body (T-10-15).
*/
export interface SaveMyCredentialPayload {
providerType: 'caldav';
fastmailEmail: string;
appPassword: string;
}
/**
* Fetch the list of all members with their credential status.
* Admin-only: the server enforces requireAdmin (403 for non-admins).
*/
export async function fetchAdminMembers(): Promise<AdminMembersResponse> {
const res = await fetch('/api/admin/members', {
credentials: 'include',
redirect: 'manual',
});
handleAuthResponse(res, 'GET /api/admin/members');
return res.json() as Promise<AdminMembersResponse>;
}
/**
* Save (add or rotate) a credential for any member.
* Admin-only: requires userId in payload; server enforces requireAdmin.
* The app password is sent in the request body and NEVER stored client-side.
*/
export async function saveCredential(payload: SaveCredentialPayload): Promise<void> {
const res = await fetch('/api/admin/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
redirect: 'manual',
body: JSON.stringify(payload),
});
handleAuthResponse(res, 'POST /api/admin/credentials');
}
/**
* Fetch the list of synced calendars with their shared status.
* Admin-only: the server enforces requireAdmin (403 for non-admins).
*/
export async function fetchAdminCalendars(): Promise<AdminCalendarsResponse> {
const res = await fetch('/api/admin/calendars', {
credentials: 'include',
redirect: 'manual',
});
handleAuthResponse(res, 'GET /api/admin/calendars');
return res.json() as Promise<AdminCalendarsResponse>;
}
/**
* Set the shared calendar (ADMIN-02: exclusive single-select, D-06).
* Admin-only: the server enforces requireAdmin.
* Clears is_shared on all other calendars and sets it on the given id.
*/
export async function setSharedCalendar(calendarId: number): Promise<void> {
const res = await fetch(`/api/admin/calendars/${calendarId}/shared`, {
method: 'PUT',
credentials: 'include',
redirect: 'manual',
});
handleAuthResponse(res, `PUT /api/admin/calendars/${calendarId}/shared`);
}
/**
* Self-service: save (add) the current member's own credential.
* Member-scoped: NO userId in payload — server resolves from session (Pitfall 6 / T-10-12).
* The app password is sent in the request body and NEVER stored client-side.
* On success, /api/me re-fetched via ['me'] cache invalidation → needsProviderSetup becomes false.
*/
export async function saveMyCredential(payload: SaveMyCredentialPayload): Promise<void> {
const res = await fetch('/api/me/credential', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
redirect: 'manual',
body: JSON.stringify(payload),
});
handleAuthResponse(res, 'POST /api/me/credential');
}