Phase 10 — Admin Role & Settings (ADMIN-01/02/03) #17
@@ -63,6 +63,8 @@ export interface MeUser {
|
||||
id: number;
|
||||
displayName: string | null;
|
||||
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 {
|
||||
@@ -325,3 +327,137 @@ export async function fetchWritableCalendars(): Promise<WritableCalendar[]> {
|
||||
const body = (await res.json()) as { calendars: WritableCalendar[] };
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user