refactor(api): extract account/auth/setup/admin; façade is now spread-only (R7.5 S2)
Move the last four inline domains out of lib/api.ts into per-domain slices, each owning its types + method slice. The barrel now holds ZERO endpoint logic and zero types of its own — it only re-exports core's HTTP machinery, re-exports each domain's types, and spreads the 15 slices into the single `api` object. A new endpoint therefore has nowhere to land but a domain module. The `api` shape and all call sites are unchanged (every req/beacon call byte-identical to dev; no method dropped, no key collision).
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
// The admin surface: discovery purge, system configuration, users & roles, the audit log, the
|
||||
// scheduler + maintenance controls, the demo-account whitelist, and access invites. Only touches
|
||||
// core's req. (Grouped by sub-area below; kept in one module as the cohesive "admin" domain.)
|
||||
import { req } from "./core";
|
||||
|
||||
export interface DemoWhitelistEntry {
|
||||
id: number;
|
||||
email: string;
|
||||
note: string | null;
|
||||
added_by: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Admin Configuration page: one DB-overridable setting (registry entry on the backend).
|
||||
export interface ConfigItem {
|
||||
key: string;
|
||||
type: "int" | "str" | "bool";
|
||||
group: string;
|
||||
secret: boolean;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
is_set: boolean; // a DB override row exists (else using the env/config default)
|
||||
allow_empty?: boolean; // if true, a blank field stores "" (explicit empty) rather than resetting
|
||||
value?: string | number | boolean; // non-secret: effective value
|
||||
default?: string | number | boolean; // non-secret: env/config default
|
||||
default_is_set?: boolean; // secret: whether an env default exists
|
||||
}
|
||||
export interface SystemConfigData {
|
||||
groups: Record<string, ConfigItem[]>;
|
||||
secrets_manageable: boolean;
|
||||
}
|
||||
|
||||
// Admin Users & roles page.
|
||||
export interface AdminUserRow {
|
||||
id: number;
|
||||
email: string;
|
||||
display_name: string | null;
|
||||
role: string; // "user" | "admin"
|
||||
is_active: boolean;
|
||||
is_suspended: boolean;
|
||||
email_verified: boolean;
|
||||
is_demo: boolean;
|
||||
has_password: boolean;
|
||||
has_google: boolean;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface AuditRow {
|
||||
id: number;
|
||||
created_at: string | null;
|
||||
actor: string | null; // email of the acting admin; null = System (scheduler/background)
|
||||
action: string; // canonical key, localized on the client via auditActions.<action>
|
||||
target_type: string | null;
|
||||
target_id: string | null;
|
||||
summary: string | null;
|
||||
before: Record<string, unknown> | null;
|
||||
after: Record<string, unknown> | null;
|
||||
}
|
||||
export interface AuditListResult {
|
||||
items: AuditRow[];
|
||||
total: number; // total rows in the log (may exceed `items` if capped)
|
||||
returned: number;
|
||||
}
|
||||
|
||||
export interface SchedulerJob {
|
||||
id: string;
|
||||
interval_minutes: number;
|
||||
next_run: string | null;
|
||||
running: boolean;
|
||||
status: "ok" | "error" | "skipped" | null;
|
||||
last_started: string | null;
|
||||
last_finished: string | null;
|
||||
last_result: string | null;
|
||||
last_error: string | null;
|
||||
// Live progress while running (null when idle or for jobs that don't report it).
|
||||
// total is null for indeterminate progress (e.g. enrichment drains an unknown queue).
|
||||
progress: { current: number; total: number | null; phase: string | null } | null;
|
||||
}
|
||||
|
||||
export interface SchedulerStatus {
|
||||
running_here: boolean;
|
||||
enabled: boolean;
|
||||
paused: boolean;
|
||||
jobs: SchedulerJob[];
|
||||
queue: {
|
||||
channels_recent_pending: number;
|
||||
channels_deep_pending: number;
|
||||
deep_eta_seconds: number;
|
||||
videos_pending_enrich: number;
|
||||
videos_pending_shorts: number;
|
||||
videos_live_refresh: number;
|
||||
};
|
||||
quota: {
|
||||
used_today: number;
|
||||
remaining_today: number;
|
||||
daily_budget: number;
|
||||
backfill_reserve: number;
|
||||
};
|
||||
maintenance: {
|
||||
revalidate_batch: number;
|
||||
revalidate_batch_default: number;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
// --- discovery / system configuration ---
|
||||
// Admin: reclaim all un-kept discovery content (search results + explored channels) now.
|
||||
purgeDiscovery: (): Promise<Record<string, number>> =>
|
||||
req("/api/admin/purge-discovery", { method: "POST" }),
|
||||
adminConfig: (): Promise<SystemConfigData> => req("/api/admin/config"),
|
||||
setConfig: (key: string, value: string | number | boolean): Promise<SystemConfigData> =>
|
||||
req(`/api/admin/config/${key}`, { method: "PATCH", body: JSON.stringify({ value }) }),
|
||||
resetConfig: (key: string): Promise<SystemConfigData> =>
|
||||
req(`/api/admin/config/${key}`, { method: "DELETE" }),
|
||||
testEmail: (): Promise<{ sent: boolean; to: string }> =>
|
||||
req("/api/admin/config/test-email", { method: "POST" }),
|
||||
// --- users & roles ---
|
||||
adminAudit: (limit = 500): Promise<AuditListResult> => req(`/api/admin/audit?limit=${limit}`),
|
||||
clearAudit: (): Promise<{ cleared: number }> => req("/api/admin/audit", { method: "DELETE" }),
|
||||
adminUsers: (): Promise<AdminUserRow[]> => req("/api/admin/users"),
|
||||
setUserRole: (id: number, role: "user" | "admin"): Promise<AdminUserRow> =>
|
||||
req(`/api/admin/users/${id}/role`, { method: "PATCH", body: JSON.stringify({ role }) }),
|
||||
setUserSuspended: (id: number, suspended: boolean): Promise<AdminUserRow> =>
|
||||
req(`/api/admin/users/${id}/suspend`, { method: "PATCH", body: JSON.stringify({ suspended }) }),
|
||||
adminDeleteUser: (id: number): Promise<{ deleted: number }> =>
|
||||
req(`/api/admin/users/${id}`, { method: "DELETE" }),
|
||||
// --- scheduler + maintenance ---
|
||||
schedulerStatus: (): Promise<SchedulerStatus> => req("/api/admin/scheduler"),
|
||||
updateSchedulerJob: (
|
||||
jobId: string,
|
||||
intervalMinutes: number,
|
||||
): Promise<{ id: string; interval_minutes: number }> =>
|
||||
req(`/api/admin/scheduler/jobs/${jobId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ interval_minutes: intervalMinutes }),
|
||||
}),
|
||||
runSchedulerJob: (jobId: string): Promise<{ id: string; started: boolean }> =>
|
||||
req(`/api/admin/scheduler/jobs/${jobId}/run`, { method: "POST" }),
|
||||
runAllSchedulerJobs: (): Promise<{ started: string[] }> =>
|
||||
req("/api/admin/scheduler/run-all", { method: "POST" }),
|
||||
updateMaintenanceBatch: (revalidateBatch: number): Promise<{ revalidate_batch: number }> =>
|
||||
req("/api/admin/scheduler/maintenance", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ revalidate_batch: revalidateBatch }),
|
||||
}),
|
||||
// --- demo whitelist ---
|
||||
adminDemoWhitelist: (): Promise<DemoWhitelistEntry[]> => req("/api/admin/demo/whitelist"),
|
||||
addDemoWhitelist: (email: string): Promise<DemoWhitelistEntry> =>
|
||||
req("/api/admin/demo/whitelist", { method: "POST", body: JSON.stringify({ email }) }),
|
||||
removeDemoWhitelist: (id: number) => req(`/api/admin/demo/whitelist/${id}`, { method: "DELETE" }),
|
||||
resetDemo: (): Promise<{ reset: boolean; playlists_seeded: number }> =>
|
||||
req("/api/admin/demo/reset", { method: "POST" }),
|
||||
// --- access invites ---
|
||||
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 }) }),
|
||||
};
|
||||
Reference in New Issue
Block a user