This commit is contained in:
k
2026-09-21 15:08:59 +02:00
parent 1de54b3caf
commit bec38d9f34
10 changed files with 68 additions and 22 deletions
+4
View File
@@ -44,3 +44,7 @@ Filtering, by account timetamp, alphabetily filtered, unverified/etc...
---(NEW) BUGS WITH SETTINGS:
glossian blur its diffent at each settings section opened: need to unify it
padding for buttons into admin center at pagination is so tigh. fix it
--- USERLIST:
pouvoir
+5 -1
View File
@@ -63,7 +63,11 @@ catalog), **Access** (access-request wishlist triage, see below),
working **Register** button — anyone can create an account (key shown once)
via `POST /api/auth/register`. When off, the button is greyed with the
tooltip *"Registration currently disabled on this platform"* and the
endpoint answers 403. Flipped live, no restart.
endpoint answers 403. Flipped live, no restart. Turning it on retires the
access-request wishlist automatically.
- `registration_request_enabled` (default on): shows **Request access** on the
login page (wishlist). Only switchable on while public registration is off —
the two are mutually exclusive (API answers 409 otherwise).
- `tools_ocr_enabled` (default on): admin kill-switch for the OCR tool.
- Flags live in the `settings` SQLite table, edited via
`GET/PATCH /api/admin/settings` (admin Bearer only).
+22 -3
View File
@@ -25,7 +25,7 @@ const TABS: { id: Tab; label: string; icon: typeof Users }[] = [
export function AdminCenter() {
const [allowed, setAllowed] = useState<boolean | null>(null);
const [tab, setTab] = useState<Tab>("accounts");
const [settings, setSettings] = useState<{ registrationEnabled: boolean; ocrEnabled: boolean } | null>(null);
const [settings, setSettings] = useState<{ registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } | null>(null);
const [mail, setMail] = useState<{ recovery: boolean; from?: string } | null>(null);
const [error, setError] = useState("");
const [saving, setSaving] = useState(false);
@@ -45,7 +45,7 @@ export function AdminCenter() {
.catch(() => setAllowed(false));
}, []);
async function toggle(patch: { registrationEnabled?: boolean; ocrEnabled?: boolean }) {
async function toggle(patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) {
setSaving(true);
setError("");
try {
@@ -129,10 +129,29 @@ export function AdminCenter() {
<div className="flex items-center gap-4 rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">Public registration</p>
<p className="text-sm opacity-60">Show the Register button on the login page. Off = admin-created accounts only.</p>
<p className="text-sm opacity-60">Show the Register button on the login page. Off = admin-created accounts only. Turning it on retires the wishlist below.</p>
</div>
<Switch label="Public registration" checked={settings?.registrationEnabled ?? false} onChange={(v) => toggle({ registrationEnabled: v })} />
</div>
<div className={cn(
"flex items-center gap-4 rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900",
settings?.registrationEnabled && "opacity-60",
)}>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">Access requests (wishlist)</p>
<p className="text-sm opacity-60">
{settings?.registrationEnabled
? "Unavailable while public registration is on — it replaces the wishlist."
: "Show Request access on the login page. Visitors reserve a username and plead their case; you triage them in the Access tab."}
</p>
</div>
<Switch
label="Access requests"
checked={settings?.accessRequestEnabled ?? false}
disabled={settings?.registrationEnabled ?? false}
onChange={(v) => toggle({ accessRequestEnabled: v })}
/>
</div>
<div className="flex items-center gap-4 rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">OCR tool</p>
+1 -1
View File
@@ -301,7 +301,7 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
)}
{!showCreate ? (
<div className="mb-3 flex flex-wrap gap-2">
<div className="mb-5 flex flex-wrap gap-2">
<Button variant="secondary" size="sm" onClick={() => setShowCreate(true)}>
<UserPlus size={15} /> New account
</Button>
+1 -1
View File
@@ -153,7 +153,7 @@ export function Chat({
<div className="mx-auto w-full max-w-3xl space-y-5">
{messages.length === 0 && !streaming && (
<div className="py-16 text-center">
<FlowerMark size={56} className="mx-auto mb-4" />
<FlowerMark size={56} dynamic={false} className="mx-auto mb-4" />
<h2 className="text-2xl font-bold tracking-tight">How can I help, {user.displayName.split(" ")[0]}?</h2>
<p className="mt-1 text-sm opacity-60">Pick a model, then type below. Markdown is supported.</p>
</div>
+5 -3
View File
@@ -22,12 +22,14 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
const [registerOpen, setRegisterOpen] = useState(false);
const [registrationOn, setRegistrationOn] = useState(false);
const [requestOpen, setRequestOpen] = useState(false);
const [requestOn, setRequestOn] = useState(false);
useEffect(() => {
api.methods().then((m) => {
setRecoveryOn(m.recovery);
setRecoveryFrom(m.from);
setRegistrationOn(m.registration);
setRequestOn(m.accessRequest);
}).catch(() => {});
}, []);
@@ -115,9 +117,9 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
onClose={() => setRegisterOpen(false)}
onDone={(u, k) => { setUsername(u); setKey(k); }}
/>
{/* Wishlist path: when open registration is off, visitors can still
reserve a username and plead their case; an admin triages it. */}
{!registrationOn && (
{/* Wishlist path: only when open registration is off AND the admin
enabled access requests. They are mutually exclusive server-side. */}
{!registrationOn && requestOn && (
<Button
variant="secondary"
size="lg"
+2 -5
View File
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react";
import {
Flower,
LayoutDashboard,
MessageSquarePlus,
MoreVertical,
@@ -15,7 +14,7 @@ import {
} from "lucide-react";
import type { Conversation, User } from "../lib/types.ts";
import { api } from "../lib/api.ts";
import { Avatar, Button, ContextMenu, IconButton } from "./ui.tsx";
import { Avatar, Button, ContextMenu, FlowerMark, IconButton } from "./ui.tsx";
import { cn } from "../lib/cn.ts";
interface MenuState {
@@ -127,9 +126,7 @@ export function Sidebar({
)}
>
<div className="flex items-center gap-2 px-2 pb-1 pt-0.5">
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-2xl bg-accent-600/10 text-accent-600 dark:text-accent-400">
<Flower size={20} />
</span>
<FlowerMark size={36} dynamic={false} />
<span className="font-serif text-xl font-bold tracking-tight">KisAssistant</span>
</div>
<div className="flex items-center gap-1">
+4 -4
View File
@@ -491,7 +491,7 @@ export function FlowerMark({ size, className, dynamic = true }: { size?: number;
);
}
/** Assistant identity: flower mark on a soft circle (message avatars, headers). */
/** Assistant identity: static flower mark on a soft circle (message avatars). */
export function AssistantAvatar({ size = 32 }: { size?: number }) {
return (
<span
@@ -499,15 +499,15 @@ export function AssistantAvatar({ size = 32 }: { size?: number }) {
style={{ width: size, height: size }}
aria-label="KisAssistant"
>
<FlowerMark size={Math.round(size * 0.78)} />
<FlowerMark size={Math.round(size * 0.78)} dynamic={false} />
</span>
);
}
/* ---------- Switch (feature toggle) ---------- */
export function Switch({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string }) {
return <Toggle checked={checked} onChange={onChange} label={label} />;
export function Switch({ checked, onChange, label, disabled = false }: { checked: boolean; onChange: (v: boolean) => void; label: string; disabled?: boolean }) {
return <Toggle checked={checked} onChange={onChange} label={label} disabled={disabled} />;
}
/* ---------- Misc ---------- */
+4 -4
View File
@@ -67,7 +67,7 @@ export const api = {
verify: () => req<{ ok: boolean; user: User }>("/api/auth/verify"),
logout: () => req<{ ok: boolean }>("/api/auth/logout", { method: "POST" }).catch(() => ({ ok: true as const })),
methods: () => req<{ recovery: boolean; from?: string; registration: boolean }>("/api/auth/methods", {}, false),
methods: () => req<{ recovery: boolean; from?: string; registration: boolean; accessRequest: boolean }>("/api/auth/methods", {}, false),
register: (u: { username: string; displayName?: string; email?: string }) =>
req<{ user: User; key: string }>("/api/auth/register", { method: "POST", body: JSON.stringify(u) }, false),
recover: (username: string) =>
@@ -98,9 +98,9 @@ export const api = {
adminPatch: (id: string, patch: { displayName?: string; avatarUrl?: string; theme?: string; email?: string }) =>
req<{ user: User }>(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings"),
adminPatchSettings: (patch: { registrationEnabled?: boolean; ocrEnabled?: boolean }) =>
req<{ settings: { registrationEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings"),
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) =>
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
convs: () => req<{ conversations: Conversation[] }>("/api/conversations"),
/** Search your old chats (titles, topics and message content). */
+20
View File
@@ -318,6 +318,7 @@ const server = Bun.serve({
recovery: mailEnabled(),
from: mailEnabled() ? config.smtpFrom : undefined,
registration: isRegistrationEnabled(),
accessRequest: getSetting("registration_request_enabled", "1") === "1",
});
}
@@ -407,6 +408,12 @@ const server = Bun.serve({
if (path === "/api/access/request" && req.method === "POST") {
const ip = clientIp(req, server);
if (isRateLimited(ip)) return json({ error: "too many attempts, try again later" }, 429);
// Wishlist is mutually exclusive with open registration: when anyone
// can register, there is nothing to request.
if (isRegistrationEnabled() || getSetting("registration_request_enabled", "1") !== "1") {
recordAttempt(ip);
return json({ error: "access requests are currently disabled on this platform" }, 403);
}
const { ok, body } = await readJson(req);
if (!ok) {
recordAttempt(ip);
@@ -604,6 +611,7 @@ const server = Bun.serve({
return json({
settings: {
registrationEnabled: isRegistrationEnabled(),
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
},
});
@@ -616,6 +624,17 @@ const server = Bun.serve({
if (body.registrationEnabled !== undefined) {
if (typeof body.registrationEnabled !== "boolean") return json({ error: "registrationEnabled must be boolean" }, 400);
setSetting("registration_enabled", body.registrationEnabled ? "1" : "0");
// Opening registration retires the wishlist automatically.
if (body.registrationEnabled) setSetting("registration_request_enabled", "0");
}
if (body.accessRequestEnabled !== undefined) {
if (typeof body.accessRequestEnabled !== "boolean") return json({ error: "accessRequestEnabled must be boolean" }, 400);
// Mutually exclusive with open registration: the wishlist only
// makes sense when anyone-can-register is off.
if (body.accessRequestEnabled && (body.registrationEnabled ?? isRegistrationEnabled())) {
return json({ error: "disable public registration first — the wishlist replaces it" }, 409);
}
setSetting("registration_request_enabled", body.accessRequestEnabled ? "1" : "0");
}
if (body.ocrEnabled !== undefined) {
if (typeof body.ocrEnabled !== "boolean") return json({ error: "ocrEnabled must be boolean" }, 400);
@@ -624,6 +643,7 @@ const server = Bun.serve({
return json({
settings: {
registrationEnabled: isRegistrationEnabled(),
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
},
});