feat(10-04): extend client.ts — MeUser.isAdmin+needsProviderSetup + admin/self-service fetchers
- Add isAdmin and needsProviderSetup to MeUser interface (D-03 UX gating) - Add AdminMember, SaveCredentialPayload, AdminCalendar, SaveMyCredentialPayload types - Add fetchAdminMembers, saveCredential, fetchAdminCalendars, setSharedCalendar fetchers - Add saveMyCredential (self-service, no userId field — T-10-12/Pitfall 6) - All fetchers use credentials:'include', redirect:'manual', handleAuthResponse - Password never logged or stored beyond in-flight request body (T-10-15)
This commit is contained in:
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user