mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-09-04 12:46:27 +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:
@@ -392,6 +392,21 @@ a:hover {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Inline text button (the auth gate's "Forgot password?" / mode switches). */
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--lw-blue-200, #90cdf4);
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.btn-link:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ---------------- Callout ---------------- */
|
||||
.callout {
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/* Public set-password page — where invite and reset links land (/reset?token=…).
|
||||
* Outside the auth gate by design: the person arriving here has no session yet.
|
||||
* Success stores the fresh session token and drops the user into the dashboard. */
|
||||
"use client";
|
||||
|
||||
import { Suspense, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { authApi, type ApiError, type SessionResponse } from "@/lib/api";
|
||||
|
||||
function ResetForm() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const token = params.get("token") ?? "";
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
if (password !== confirm) {
|
||||
setError("Passwords don't match.");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const session = await authApi<SessionResponse>("/auth/reset", { token, password });
|
||||
window.localStorage.setItem("handler_token", session.token);
|
||||
router.replace("/");
|
||||
} catch (err) {
|
||||
setError((err as ApiError).message || "Something went wrong.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gate">
|
||||
<form className="gate-card" onSubmit={submit}>
|
||||
<div className="gate-brand">
|
||||
<span className="logo" style={{ width: 26, height: 26, borderRadius: 7 }} />
|
||||
Claude Monitor
|
||||
</div>
|
||||
{token ? (
|
||||
<>
|
||||
<p className="muted" style={{ fontSize: "var(--text-sm)", margin: 0 }}>
|
||||
Choose a password for your account. The link you followed is one-shot — once
|
||||
set, sign in with your email and this password.
|
||||
</p>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="New password (8+ characters)"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
minLength={8}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
{error && (
|
||||
<p className="callout callout-danger" style={{ margin: 0 }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button className="btn btn-primary" type="submit" disabled={busy}>
|
||||
Set password and sign in
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p className="callout callout-danger" style={{ margin: 0 }}>
|
||||
This page needs a reset link (…/reset?token=…). Ask an admin for one, or use
|
||||
“Forgot password?” on the sign-in page.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPage() {
|
||||
// useSearchParams requires a Suspense boundary under the static export.
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ResetForm />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Users page (admin only — the sidebar hides it otherwise; the API enforces it). */
|
||||
"use client";
|
||||
|
||||
import { UsersSection } from "@/components/sections/UsersSection";
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<div className="main-scroll">
|
||||
<UsersSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user