feat(m5c): onboarding — DB invites, request-access, admin approval, email
Move the access whitelist from the ALLOWED_EMAILS env var into a DB Invite table (env kept as bootstrap fallback), and add a self-service request + admin approval flow with fail-soft email. - models: Invite(email, status pending|approved|denied, requested_at, decided_*) - migration 0008: invites table; seed env ALLOWED_EMAILS u ADMIN_EMAILS as approved - auth: is_allowed() (DB-first, env fallback); a denied Google login records a pending request and bounces to /?access=requested instead of a raw 403; public POST /auth/request-access; upsert is idempotent so repeats don't re-spam admins - routes/admin.py (admin-only): list/approve/deny invites + manual add - email.py: smtplib + Gmail App Password, fail-soft (skips if SMTP unset) - /api/me exposes pending_invites; config + .env.example gain SMTP_* - UI: Login 'Request access' form + access=requested/denied handling; Settings -> Access requests (approve/deny + add); admin nudge toast on pending requests Verified locally: request-access creates a pending invite and emails the admin; seed approved npeter83; guinea-pig yt.trash2023 denied until approved.
This commit is contained in:
parent
a4b1ea5c19
commit
d9caf12202
13 changed files with 605 additions and 15 deletions
|
|
@ -7,9 +7,20 @@ export interface Me {
|
|||
avatar_url: string | null;
|
||||
role: string;
|
||||
can_write: boolean;
|
||||
pending_invites: number;
|
||||
preferences: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Invite {
|
||||
id: number;
|
||||
email: string;
|
||||
status: "pending" | "approved" | "denied";
|
||||
note: string | null;
|
||||
requested_at: string | null;
|
||||
decided_at: string | null;
|
||||
decided_by: string | null;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -203,6 +214,17 @@ export const api = {
|
|||
req(`/api/channels/${id}/subscription`, { method: "DELETE" }),
|
||||
syncSubscriptions: () => req("/api/sync/subscriptions", { method: "POST" }),
|
||||
|
||||
// --- onboarding / admin ---
|
||||
requestAccess: (email: string): Promise<{ status: string }> =>
|
||||
req("/auth/request-access", { method: "POST", body: JSON.stringify({ email }) }),
|
||||
adminInvites: (status?: string): Promise<Invite[]> =>
|
||||
req(`/api/admin/invites${status ? `?status=${status}` : ""}`),
|
||||
approveInvite: (id: number) =>
|
||||
req(`/api/admin/invites/${id}/approve`, { method: "POST" }),
|
||||
denyInvite: (id: number) => req(`/api/admin/invites/${id}/deny`, { method: "POST" }),
|
||||
addInvite: (email: string) =>
|
||||
req("/api/admin/invites", { method: "POST", body: JSON.stringify({ email }) }),
|
||||
|
||||
// --- user tags ---
|
||||
createTag: (t: { name: string; color?: string; category?: string }) =>
|
||||
req("/api/tags", { method: "POST", body: JSON.stringify(t) }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue