mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:36:24 +00:00
Frontend: email sign-in, first-run setup, reset links, Users admin page
- AuthGate replaces the raw token prompt: first-run setup form (creates the admin) when no accounts exist, email/password sign-in with a forgot-password flow, and a collapsible raw-API-token fallback for legacy/script setups. - /reset is a public page where invite and password-reset links land; success stores the fresh session and enters the dashboard. - Users section (admin-only nav): invite by email (link always shown, emailed when SMTP is configured), admin/disable toggles, reset links, and delete with the shared-resources handoff spelled out. - Sidebar shows who is signed in; sign-out revokes the session server-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
This commit is contained in:
@@ -15,6 +15,8 @@ export interface Project {
|
||||
root_dir: string;
|
||||
git_remote?: string | null;
|
||||
credential_ref?: string | null;
|
||||
/* Owning user account; null = shared/legacy (visible to everyone, admin-managed). */
|
||||
owner_user_id?: number | null;
|
||||
created_at: string;
|
||||
/* Present on the registration response in git-server mode: the enqueued clone. */
|
||||
sync_command_id?: number | null;
|
||||
@@ -156,6 +158,7 @@ export interface ClaudeSkill {
|
||||
/* Relative paths of auxiliary files captured by an install-from-prompt import
|
||||
* (references/, scripts/, …); synced alongside SKILL.md, read-only here. */
|
||||
files: string[];
|
||||
owner_user_id?: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -172,6 +175,7 @@ export interface ClaudeConnector {
|
||||
url?: string | null;
|
||||
headers?: Record<string, string> | null;
|
||||
enabled: boolean;
|
||||
owner_user_id?: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -181,6 +185,7 @@ export interface ClaudePlugin {
|
||||
marketplace: string;
|
||||
marketplace_repo: string;
|
||||
enabled: boolean;
|
||||
owner_user_id?: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -199,6 +204,7 @@ export interface ClaudeModel {
|
||||
env?: Record<string, string> | null;
|
||||
enabled: boolean;
|
||||
has_api_key: boolean;
|
||||
owner_user_id?: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -250,6 +256,71 @@ export interface SharedContext {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/* ---- user accounts (/auth) ---- */
|
||||
|
||||
export interface AuthStatus {
|
||||
initialized: boolean; // any account exists; false => show the first-run setup form
|
||||
smtp_configured: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
is_admin: boolean;
|
||||
disabled: boolean;
|
||||
/* False until an invited user sets their password through their invite link. */
|
||||
has_password: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Me {
|
||||
kind: "user" | "token";
|
||||
user_id?: number | null;
|
||||
email?: string | null;
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
export interface SessionResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface UserCreated {
|
||||
user: User;
|
||||
invite_url: string;
|
||||
emailed: boolean;
|
||||
}
|
||||
|
||||
export interface ResetLink {
|
||||
reset_url: string;
|
||||
emailed: boolean;
|
||||
}
|
||||
|
||||
/* Unauthenticated auth calls (status/login/setup/forgot/reset) — used by the gate
|
||||
* before any token exists, so they sit outside createClient. */
|
||||
export async function authApi<T>(path: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(BASE + path, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
headers: body === undefined ? {} : { "Content-Type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let detail: string = res.statusText;
|
||||
try {
|
||||
const j = await res.json();
|
||||
if (j && typeof j.detail !== "undefined") {
|
||||
detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
||||
}
|
||||
} catch {
|
||||
/* non-JSON error body; keep statusText */
|
||||
}
|
||||
const err = new Error(detail) as ApiError;
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/* Thrown on a 401 so callers can distinguish "token rejected" from real errors and stay
|
||||
* quiet while the app re-prompts for a token. */
|
||||
export class AuthError extends Error {
|
||||
|
||||
Reference in New Issue
Block a user