This commit is contained in:
k
2026-09-21 16:02:05 +02:00
parent 133a08b7ed
commit d3780edc9c
6 changed files with 343 additions and 153 deletions
+16 -14
View File
@@ -1,17 +1,14 @@
import { useEffect, useState } from "react";
import { ArrowRight, KeyRound, MessageSquareText, ShieldCheck, User, UserPlus, Zap } from "lucide-react";
import { api, setToken } from "../lib/api.ts";
import { useT } from "../lib/i18n.ts";
import type { User as UserType } from "../lib/types.ts";
import { Button, FlowerMark, Input, Spinner } from "./ui.tsx";
import { RecoverDialog, RegisterDialog } from "./dialogs.tsx";
import { AccessRequestModal } from "./ReviewPage.tsx";
const PERKS = [
{ icon: ShieldCheck, text: "Private by design — no cookies, no tracking" },
{ icon: Zap, text: "Your own models, streamed in real time" },
];
export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
const { t } = useT();
const [username, setUsername] = useState("admin");
const [key, setKey] = useState("");
const [error, setError] = useState("");
@@ -23,6 +20,11 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
const [registrationOn, setRegistrationOn] = useState(false);
const [requestOpen, setRequestOpen] = useState(false);
const [requestOn, setRequestOn] = useState(false);
const PERKS = [
{ icon: ShieldCheck, text: t("login.perk1") },
{ icon: Zap, text: t("login.perk2") },
];
// TOTP second step (set when the server answers totpRequired).
const [totpToken, setTotpToken] = useState<string | null>(null);
const [totpUsername, setTotpUsername] = useState("");
@@ -52,7 +54,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
setToken(res.token);
onLogin(res.user);
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
setError(err instanceof Error ? err.message : t("login.failed"));
} finally {
setBusy(false);
}
@@ -80,8 +82,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
<div className="rounded-[2rem] border border-white/60 bg-white/80 p-8 shadow-2xl shadow-accent-900/5 backdrop-blur-xl dark:border-zinc-700/60 dark:bg-zinc-900/80">
<div className="mb-2 flex flex-col items-center text-center">
<FlowerMark size={68} dynamic={false} className="mb-4 drop-shadow-lg" />
<h1 className="text-2xl font-bold tracking-tight">Welcome back</h1>
<p className="mt-1 text-sm opacity-60">Sign in to KisAssistant to continue</p>
<h1 className="text-2xl font-bold tracking-tight">{t("login.welcome")}</h1>
<p className="mt-1 text-sm opacity-60">{t("login.subtitle")}</p>
</div>
{totpToken ? (
@@ -129,8 +131,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
placeholder="Username"
aria-label="Username"
placeholder={t("login.usernamePh")}
aria-label={t("login.usernamePh")}
required
/>
</div>
@@ -143,8 +145,8 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
value={key}
onChange={(e) => setKey(e.target.value)}
autoComplete="current-password"
placeholder="Access key"
aria-label="Access key"
placeholder={t("login.keyPh")}
aria-label={t("login.keyPh")}
required
/>
</div>
@@ -189,13 +191,13 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
onClick={() => setRequestOpen(true)}
>
<MessageSquareText size={16} />
Request access
{t("login.requestAccess")}
</Button>
)}
<AccessRequestModal open={requestOpen} onClose={() => setRequestOpen(false)} />
{recoveryOn && (
<button onClick={() => setRecoverOpen(true)} className="mt-3 w-full text-center text-sm opacity-60 transition hover:opacity-100 hover:underline">
Forgot your access key?
{t("login.forgot")}
</button>
)}
<RecoverDialog open={recoverOpen} onClose={() => setRecoverOpen(false)} from={recoveryFrom} />
+41 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Palette, Pencil, ScanText, Search, Settings2, ShieldCheck, Sparkles, Tag, Unplug, User as UserIcon, UserPlus, X } from "lucide-react";
import { Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Palette, Pencil, ScanText, Search, Settings2, ShieldCheck, Sparkles, Tag, Trash2, Unplug, User as UserIcon, UserPlus, X } from "lucide-react";
import { api } from "../lib/api.ts";
import { cn } from "../lib/cn.ts";
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
@@ -761,6 +761,36 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
}
async function disableTotp() {
if (code.replace(/\D/g, "").length !== 6) return;
setBusy(true);
setError("");
try {
await api.totpDisable(code.replace(/\D/g, ""));
setTotpOn(false);
setCode("");
} catch (err) {
setError(err instanceof Error ? err.message : "Invalid code");
} finally {
setBusy(false);
}
}
async function removeAccount() {
if (!armingDelete) {
setArmingDelete(true);
return;
}
setBusy(true);
setError("");
try {
await api.deleteMe();
onKeyRotated?.();
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed");
} finally {
setBusy(false);
}
}
return (
<div className="space-y-5">
@@ -845,6 +875,16 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
</span>
</div>
<div className="rounded-2xl border border-red-500/30 p-3">
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium text-red-600 dark:text-red-400">
<Trash2 size={14} /> Danger zone
</p>
<p className="mb-2 text-xs opacity-60">Permanently delete your account with all chats, shares and sessions. Cannot be undone.</p>
<Button size="sm" variant="secondary" disabled={busy} onClick={removeAccount} className="!text-red-600 dark:!text-red-400">
{armingDelete ? "Click again to permanently delete" : "Delete my account"}
</Button>
</div>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
</div>
);
+11 -7
View File
@@ -268,6 +268,7 @@ export function Modal({
dragStart.current = null;
}
}, [open ]);
const { t } = useT();
if (!open) return null;
const Icon = icon;
@@ -308,7 +309,7 @@ export function Modal({
</span>
)}
<h2 className="text-lg font-semibold">{title}</h2>
<button onClick={onClose} aria-label="Close" className="ml-auto rounded-full p-1.5 hover:bg-stone-200/60 dark:hover:bg-zinc-800">
<button onClick={onClose} aria-label={t("common.close")} className="ml-auto rounded-full p-1.5 hover:bg-stone-200/60 dark:hover:bg-zinc-800">
<X size={18} />
</button>
</div>
@@ -325,7 +326,7 @@ export function ConfirmDialog({
onConfirm,
title,
message,
confirmLabel = "Delete",
confirmLabel,
}: {
open: boolean;
onClose: () => void;
@@ -334,12 +335,13 @@ export function ConfirmDialog({
message: string;
confirmLabel?: string;
}) {
const { t } = useT();
return (
<Modal open={open} onClose={onClose} title={title}>
<p className="text-sm opacity-80">{message}</p>
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" size="sm" onClick={onClose}>Cancel</Button>
<Button variant="danger" size="sm" onClick={() => { onConfirm(); onClose(); }}>{confirmLabel}</Button>
<Button variant="secondary" size="sm" onClick={onClose}>{t("common.cancel")}</Button>
<Button variant="danger" size="sm" onClick={() => { onConfirm(); onClose(); }}>{confirmLabel ?? t("common.delete")}</Button>
</div>
</Modal>
);
@@ -559,7 +561,8 @@ export function Toggle({
);
}
export function CopyButton({ text, label = "Copy" }: { text: string; label?: string }) {
export function CopyButton({ text, label }: { text: string; label?: string }) {
const { t } = useT();
const [done, setDone] = useState(false);
return (
<button
@@ -572,7 +575,7 @@ export function CopyButton({ text, label = "Copy" }: { text: string; label?: str
className="inline-flex items-center gap-1.5 rounded-full border border-stone-200 bg-white px-3 py-1.5 text-xs font-medium transition hover:bg-stone-50 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:bg-zinc-800"
>
{done ? <Check size={13} className="text-accent-500" /> : <Copy size={13} />}
{done ? "Copied" : label}
{done ? t("common.copied") : (label ?? t("common.copy"))}
</button>
);
}
@@ -581,12 +584,13 @@ export function CopyButton({ text, label = "Copy" }: { text: string; label?: str
/** Animated blooming flower shown while the app boots (themed, mobile-safe). */
export function LoadingScreen() {
const { t } = useT();
return (
<div className="loading-enter grid min-h-full place-items-center" role="status" aria-label="Loading KisAssistant">
<div className="flex flex-col items-center gap-4 px-6 text-center">
<FlowerMark className="h-16 w-16 sm:h-20 sm:w-20" />
<p className="font-serif text-2xl font-bold tracking-tight">KisAssistant</p>
<p className="loading-dots text-sm opacity-60">Preparing your chats</p>
<p className="loading-dots text-sm opacity-60">{t("loading.tagline")}</p>
</div>
</div>
);