updates
This commit is contained in:
@@ -44,3 +44,7 @@ Filtering, by account timetamp, alphabetily filtered, unverified/etc...
|
|||||||
---(NEW) BUGS WITH SETTINGS:
|
---(NEW) BUGS WITH SETTINGS:
|
||||||
glossian blur its diffent at each settings section opened: need to unify it
|
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
|
padding for buttons into admin center at pagination is so tigh. fix it
|
||||||
|
|
||||||
|
|
||||||
|
--- USERLIST:
|
||||||
|
pouvoir
|
||||||
|
|||||||
+5
-1
@@ -63,7 +63,11 @@ catalog), **Access** (access-request wishlist triage, see below),
|
|||||||
working **Register** button — anyone can create an account (key shown once)
|
working **Register** button — anyone can create an account (key shown once)
|
||||||
via `POST /api/auth/register`. When off, the button is greyed with the
|
via `POST /api/auth/register`. When off, the button is greyed with the
|
||||||
tooltip *"Registration currently disabled on this platform"* and 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.
|
- `tools_ocr_enabled` (default on): admin kill-switch for the OCR tool.
|
||||||
- Flags live in the `settings` SQLite table, edited via
|
- Flags live in the `settings` SQLite table, edited via
|
||||||
`GET/PATCH /api/admin/settings` (admin Bearer only).
|
`GET/PATCH /api/admin/settings` (admin Bearer only).
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const TABS: { id: Tab; label: string; icon: typeof Users }[] = [
|
|||||||
export function AdminCenter() {
|
export function AdminCenter() {
|
||||||
const [allowed, setAllowed] = useState<boolean | null>(null);
|
const [allowed, setAllowed] = useState<boolean | null>(null);
|
||||||
const [tab, setTab] = useState<Tab>("accounts");
|
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 [mail, setMail] = useState<{ recovery: boolean; from?: string } | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -45,7 +45,7 @@ export function AdminCenter() {
|
|||||||
.catch(() => setAllowed(false));
|
.catch(() => setAllowed(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function toggle(patch: { registrationEnabled?: boolean; ocrEnabled?: boolean }) {
|
async function toggle(patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
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="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">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium">Public registration</p>
|
<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>
|
</div>
|
||||||
<Switch label="Public registration" checked={settings?.registrationEnabled ?? false} onChange={(v) => toggle({ registrationEnabled: v })} />
|
<Switch label="Public registration" checked={settings?.registrationEnabled ?? false} onChange={(v) => toggle({ registrationEnabled: v })} />
|
||||||
</div>
|
</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="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">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium">OCR tool</p>
|
<p className="text-sm font-medium">OCR tool</p>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export function AdminPanel({ open, onClose, bare }: { open: boolean; onClose: ()
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!showCreate ? (
|
{!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)}>
|
<Button variant="secondary" size="sm" onClick={() => setShowCreate(true)}>
|
||||||
<UserPlus size={15} /> New account
|
<UserPlus size={15} /> New account
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function Chat({
|
|||||||
<div className="mx-auto w-full max-w-3xl space-y-5">
|
<div className="mx-auto w-full max-w-3xl space-y-5">
|
||||||
{messages.length === 0 && !streaming && (
|
{messages.length === 0 && !streaming && (
|
||||||
<div className="py-16 text-center">
|
<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>
|
<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>
|
<p className="mt-1 text-sm opacity-60">Pick a model, then type below. Markdown is supported.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
const [registerOpen, setRegisterOpen] = useState(false);
|
const [registerOpen, setRegisterOpen] = useState(false);
|
||||||
const [registrationOn, setRegistrationOn] = useState(false);
|
const [registrationOn, setRegistrationOn] = useState(false);
|
||||||
const [requestOpen, setRequestOpen] = useState(false);
|
const [requestOpen, setRequestOpen] = useState(false);
|
||||||
|
const [requestOn, setRequestOn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.methods().then((m) => {
|
api.methods().then((m) => {
|
||||||
setRecoveryOn(m.recovery);
|
setRecoveryOn(m.recovery);
|
||||||
setRecoveryFrom(m.from);
|
setRecoveryFrom(m.from);
|
||||||
setRegistrationOn(m.registration);
|
setRegistrationOn(m.registration);
|
||||||
|
setRequestOn(m.accessRequest);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -115,9 +117,9 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
|
|||||||
onClose={() => setRegisterOpen(false)}
|
onClose={() => setRegisterOpen(false)}
|
||||||
onDone={(u, k) => { setUsername(u); setKey(k); }}
|
onDone={(u, k) => { setUsername(u); setKey(k); }}
|
||||||
/>
|
/>
|
||||||
{/* Wishlist path: when open registration is off, visitors can still
|
{/* Wishlist path: only when open registration is off AND the admin
|
||||||
reserve a username and plead their case; an admin triages it. */}
|
enabled access requests. They are mutually exclusive server-side. */}
|
||||||
{!registrationOn && (
|
{!registrationOn && requestOn && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Flower,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Conversation, User } from "../lib/types.ts";
|
import type { Conversation, User } from "../lib/types.ts";
|
||||||
import { api } from "../lib/api.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";
|
import { cn } from "../lib/cn.ts";
|
||||||
|
|
||||||
interface MenuState {
|
interface MenuState {
|
||||||
@@ -127,9 +126,7 @@ export function Sidebar({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 px-2 pb-1 pt-0.5">
|
<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">
|
<FlowerMark size={36} dynamic={false} />
|
||||||
<Flower size={20} />
|
|
||||||
</span>
|
|
||||||
<span className="font-serif text-xl font-bold tracking-tight">KisAssistant</span>
|
<span className="font-serif text-xl font-bold tracking-tight">KisAssistant</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -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 }) {
|
export function AssistantAvatar({ size = 32 }: { size?: number }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -499,15 +499,15 @@ export function AssistantAvatar({ size = 32 }: { size?: number }) {
|
|||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size }}
|
||||||
aria-label="KisAssistant"
|
aria-label="KisAssistant"
|
||||||
>
|
>
|
||||||
<FlowerMark size={Math.round(size * 0.78)} />
|
<FlowerMark size={Math.round(size * 0.78)} dynamic={false} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Switch (feature toggle) ---------- */
|
/* ---------- Switch (feature toggle) ---------- */
|
||||||
|
|
||||||
export function Switch({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string }) {
|
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} />;
|
return <Toggle checked={checked} onChange={onChange} label={label} disabled={disabled} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Misc ---------- */
|
/* ---------- Misc ---------- */
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const api = {
|
|||||||
verify: () => req<{ ok: boolean; user: User }>("/api/auth/verify"),
|
verify: () => req<{ ok: boolean; user: User }>("/api/auth/verify"),
|
||||||
logout: () => req<{ ok: boolean }>("/api/auth/logout", { method: "POST" }).catch(() => ({ ok: true as const })),
|
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 }) =>
|
register: (u: { username: string; displayName?: string; email?: string }) =>
|
||||||
req<{ user: User; key: string }>("/api/auth/register", { method: "POST", body: JSON.stringify(u) }, false),
|
req<{ user: User; key: string }>("/api/auth/register", { method: "POST", body: JSON.stringify(u) }, false),
|
||||||
recover: (username: string) =>
|
recover: (username: string) =>
|
||||||
@@ -98,9 +98,9 @@ export const api = {
|
|||||||
adminPatch: (id: string, patch: { displayName?: string; avatarUrl?: string; theme?: string; email?: string }) =>
|
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) }),
|
req<{ user: User }>(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||||
|
|
||||||
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings"),
|
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings"),
|
||||||
adminPatchSettings: (patch: { registrationEnabled?: boolean; ocrEnabled?: boolean }) =>
|
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) =>
|
||||||
req<{ settings: { registrationEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
|
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
|
||||||
|
|
||||||
convs: () => req<{ conversations: Conversation[] }>("/api/conversations"),
|
convs: () => req<{ conversations: Conversation[] }>("/api/conversations"),
|
||||||
/** Search your old chats (titles, topics and message content). */
|
/** Search your old chats (titles, topics and message content). */
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ const server = Bun.serve({
|
|||||||
recovery: mailEnabled(),
|
recovery: mailEnabled(),
|
||||||
from: mailEnabled() ? config.smtpFrom : undefined,
|
from: mailEnabled() ? config.smtpFrom : undefined,
|
||||||
registration: isRegistrationEnabled(),
|
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") {
|
if (path === "/api/access/request" && req.method === "POST") {
|
||||||
const ip = clientIp(req, server);
|
const ip = clientIp(req, server);
|
||||||
if (isRateLimited(ip)) return json({ error: "too many attempts, try again later" }, 429);
|
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);
|
const { ok, body } = await readJson(req);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
recordAttempt(ip);
|
recordAttempt(ip);
|
||||||
@@ -604,6 +611,7 @@ const server = Bun.serve({
|
|||||||
return json({
|
return json({
|
||||||
settings: {
|
settings: {
|
||||||
registrationEnabled: isRegistrationEnabled(),
|
registrationEnabled: isRegistrationEnabled(),
|
||||||
|
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||||
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -616,6 +624,17 @@ const server = Bun.serve({
|
|||||||
if (body.registrationEnabled !== undefined) {
|
if (body.registrationEnabled !== undefined) {
|
||||||
if (typeof body.registrationEnabled !== "boolean") return json({ error: "registrationEnabled must be boolean" }, 400);
|
if (typeof body.registrationEnabled !== "boolean") return json({ error: "registrationEnabled must be boolean" }, 400);
|
||||||
setSetting("registration_enabled", body.registrationEnabled ? "1" : "0");
|
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 (body.ocrEnabled !== undefined) {
|
||||||
if (typeof body.ocrEnabled !== "boolean") return json({ error: "ocrEnabled must be boolean" }, 400);
|
if (typeof body.ocrEnabled !== "boolean") return json({ error: "ocrEnabled must be boolean" }, 400);
|
||||||
@@ -624,6 +643,7 @@ const server = Bun.serve({
|
|||||||
return json({
|
return json({
|
||||||
settings: {
|
settings: {
|
||||||
registrationEnabled: isRegistrationEnabled(),
|
registrationEnabled: isRegistrationEnabled(),
|
||||||
|
accessRequestEnabled: getSetting("registration_request_enabled", "1") === "1",
|
||||||
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
ocrEnabled: getSetting("tools_ocr_enabled", "1") === "1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user