This commit is contained in:
k
2026-09-21 16:12:51 +02:00
parent 1352f5374e
commit f46a6b4412
5 changed files with 77 additions and 44 deletions
+7
View File
@@ -42,3 +42,10 @@ Système de service mails aussi pour les comptes (optionel) pour recovery passwo
when disabling attachment sendability: the text "Message" placeholder nto the composer chat are too tigh to the left border, make a fallback padding when no attachment button when disabling attachment sendability: the text "Message" placeholder nto the composer chat are too tigh to the left border, make a fallback padding when no attachment button
make in the settigns: an sub category: changing key, admiting TOTP key/passkey for security make in the settigns: an sub category: changing key, admiting TOTP key/passkey for security
--- bugd II:
sur la page share, l' avatar de l'utilisateur ne s'affiche pas -> fix sa
--- improvements:
éviter de ask h24 ollama pour ces modèles quand un utilisateur ce connece. c'est le backend qui sert les demande d el'utilisateurs. iL relay pas sur le ollama. Le backend vas cacher avec un TLL les modèles présent en mémire dans un objet qui est dans le backend. refresh avec un TT: toutes les minutes.
L'administrateur lui, qand il demande les modèles dams le ollama, lui, force un refresh .
+5 -5
View File
@@ -89,7 +89,7 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
{totpToken ? ( {totpToken ? (
<form onSubmit={submitTotp} className="mt-6 space-y-3"> <form onSubmit={submitTotp} className="mt-6 space-y-3">
<p className="text-center text-sm opacity-70"> <p className="text-center text-sm opacity-70">
Two-factor for <strong>@{totpUsername}</strong> — enter the 6-digit code from your authenticator app. {t("login.totpIntro", { user: "@" + totpUsername })}
</p> </p>
<div className="relative"> <div className="relative">
<KeyRound size={17} className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-stone-400 dark:text-zinc-500" /> <KeyRound size={17} className="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-stone-400 dark:text-zinc-500" />
@@ -156,13 +156,13 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
</p> </p>
)} )}
<Button type="submit" variant="gradient" size="lg" className="w-full" disabled={busy}> <Button type="submit" variant="gradient" size="lg" className="w-full" disabled={busy}>
{busy ? <Spinner /> : (<>Unlock <ArrowRight size={17} /></>)} {busy ? <Spinner /> : (<>{t("login.unlock")} <ArrowRight size={17} /></>)}
</Button> </Button>
</form> </form>
)} )}
<div className="mt-4 flex items-center gap-3 text-xs opacity-40"> <div className="mt-4 flex items-center gap-3 text-xs opacity-40">
<span className="h-px flex-1 bg-current" /> <span className="h-px flex-1 bg-current" />
New here? {t("login.newHere")}
<span className="h-px flex-1 bg-current" /> <span className="h-px flex-1 bg-current" />
</div> </div>
<Button <Button
@@ -170,11 +170,11 @@ export function Login({ onLogin }: { onLogin: (user: UserType) => void }) {
size="lg" size="lg"
className="mt-3 w-full" className="mt-3 w-full"
disabled={!registrationOn} disabled={!registrationOn}
title={registrationOn ? "Create an account" : "Registration currently disabled on this platform"} title={registrationOn ? t("login.regTipOn") : t("login.regTipOff")}
onClick={() => setRegisterOpen(true)} onClick={() => setRegisterOpen(true)}
> >
<UserPlus size={16} /> <UserPlus size={16} />
Register {t("login.register")}
</Button> </Button>
<RegisterDialog <RegisterDialog
open={registerOpen} open={registerOpen}
+28 -22
View File
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn.ts";
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts"; import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
import { FEATURES, setFeature, useFeatures } from "../lib/features.ts"; import { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
import type { Conversation, FilePreview, User } from "../lib/types.ts"; import type { Conversation, FilePreview, User } from "../lib/types.ts";
import { Avatar, bumpCdnVersion, Button, CopyButton, Field, Input, Modal, Picker, Spinner, Toggle } from "./ui.tsx"; import { Avatar, bumpCdnVersion, Button, ConfirmDialog, CopyButton, Field, Input, Modal, Picker, Spinner, Toggle } from "./ui.tsx";
/* ---------- Rename + topic ---------- */ /* ---------- Rename + topic ---------- */
@@ -705,7 +705,7 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [rotatedKey, setRotatedKey] = useState<string | null>(null); const [rotatedKey, setRotatedKey] = useState<string | null>(null);
const [armingRotate, setArmingRotate] = useState(false); const [armingRotate, setArmingRotate] = useState(false);
const [armingDelete, setArmingDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
useEffect(() => { useEffect(() => {
api.totpStatus().then((r) => setTotpOn(r.enabled)).catch(() => setTotpOn(false)); api.totpStatus().then((r) => setTotpOn(r.enabled)).catch(() => setTotpOn(false));
@@ -776,14 +776,11 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
} }
async function removeAccount() { async function removeAccount() {
if (!armingDelete) {
setArmingDelete(true);
return;
}
setBusy(true); setBusy(true);
setError(""); setError("");
try { try {
await api.deleteMe(); await api.deleteMe();
setConfirmDelete(false);
onKeyRotated?.(); onKeyRotated?.();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Delete failed"); setError(err instanceof Error ? err.message : "Delete failed");
@@ -796,24 +793,10 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
<div className="space-y-5"> <div className="space-y-5">
<div> <div>
<p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><KeyRound size={14} className="opacity-60" /> Access key</p> <p className="mb-1 flex items-center gap-1.5 text-sm font-medium"><KeyRound size={14} className="opacity-60" /> Access key</p>
{rotatedKey ? (
<div className="rounded-2xl border border-accent-500/40 bg-accent-50 p-3 dark:bg-accent-950/30">
<p className="mb-2 text-sm font-medium">New key — copy it now, it won't be shown again:</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 truncate rounded-xl bg-white px-3 py-2 text-sm dark:bg-zinc-900">{rotatedKey}</code>
<CopyButton text={rotatedKey} />
</div>
<p className="mt-2 text-xs opacity-70">Your old sessions are revoked. Log back in with the new key.</p>
<Button size="sm" className="mt-2 w-full" onClick={() => onKeyRotated?.()}>Done — log me out</Button>
</div>
) : (
<>
<p className="mb-2 text-xs opacity-60">Rotate your access key. Old sessions are revoked immediately — you'll log back in with the new key.</p> <p className="mb-2 text-xs opacity-60">Rotate your access key. Old sessions are revoked immediately — you'll log back in with the new key.</p>
<Button size="sm" variant="secondary" disabled={busy} onClick={rotate}> <Button size="sm" variant="secondary" disabled={busy} onClick={rotate}>
{armingRotate ? "Click again to confirm rotation" : "Rotate my access key"} {armingRotate ? "Click again to confirm rotation" : "Rotate my access key"}
</Button> </Button>
</>
)}
</div> </div>
<div> <div>
@@ -880,12 +863,35 @@ function SecuritySection({ onKeyRotated }: { onKeyRotated?: () => void }) {
<Trash2 size={14} /> Danger zone <Trash2 size={14} /> Danger zone
</p> </p>
<p className="mb-2 text-xs opacity-60">Permanently delete your account with all chats, shares and sessions. Cannot be undone.</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"> <Button size="sm" variant="secondary" disabled={busy} onClick={() => setConfirmDelete(true)} className="!text-red-600 dark:!text-red-400">
{armingDelete ? "Click again to permanently delete" : "Delete my account"} Delete my account
</Button> </Button>
</div> </div>
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>} {error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
<Modal open={rotatedKey !== null} onClose={() => {}} title="New access key" icon={KeyRound}>
<div className="space-y-4">
<p className="text-sm opacity-80">Copy it now — it won't be shown again. Your old sessions are revoked.</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 truncate rounded-2xl bg-stone-100 px-3 py-2.5 font-mono text-sm dark:bg-zinc-800">{rotatedKey}</code>
{rotatedKey && <CopyButton text={rotatedKey} />}
</div>
<Button size="sm" className="w-full" onClick={() => onKeyRotated?.()}>Done — log me out</Button>
</div>
</Modal>
<ConfirmDialog
open={confirmDelete}
onClose={() => setConfirmDelete(false)}
onConfirm={removeAccount}
title="Delete your account?"
message="This permanently removes your account with all conversations, shares and sessions. This cannot be undone."
confirmLabel="Delete forever"
/>
</div>
);
}
</div> </div>
); );
} }
+31 -11
View File
@@ -56,8 +56,12 @@ const en = {
"login.forgot": "Forgot your access key?", "login.forgot": "Forgot your access key?",
"login.perk1": "Private by design — no cookies, no tracking", "login.perk1": "Private by design — no cookies, no tracking",
"login.perk2": "Your own models, streamed in real time", "login.perk2": "Your own models, streamed in real time",
"login.adminNote": "login.adminNotePre": "Admin key lives in the server ",
"Admin key lives in the server .env; user keys are issued by the admin.", "login.adminNotePost": "; user keys are issued by the admin.",
"login.totpIntro": "Two-factor for @{user} — enter the 6-digit code from your authenticator app.",
"login.totpAria": "Authenticator code",
"login.totpInvalid": "Invalid code",
"login.verify": "Verify",
"sidebar.newChat": "New chat", "sidebar.newChat": "New chat",
"sidebar.collapse": "Collapse sidebar", "sidebar.collapse": "Collapse sidebar",
@@ -357,8 +361,12 @@ const es: Dict = {
"login.forgot": "¿Olvidaste tu clave de acceso?", "login.forgot": "¿Olvidaste tu clave de acceso?",
"login.perk1": "Privado por diseño — sin cookies ni rastreo", "login.perk1": "Privado por diseño — sin cookies ni rastreo",
"login.perk2": "Tus propios modelos, en tiempo real", "login.perk2": "Tus propios modelos, en tiempo real",
"login.adminNote": "login.adminNotePre": "La clave de admin vive en el .env del servidor",
"La clave de admin vive en el .env del servidor; las claves de usuario las emite el admin.", "login.adminNotePost": "; las claves de usuario las emite el admin.",
"login.totpIntro": "Doble factor para @{user} — escribe el código de 6 dígitos de tu app.",
"login.totpAria": "Código de autenticación",
"login.totpInvalid": "Código inválido",
"login.verify": "Verificar",
"sidebar.newChat": "Nuevo chat", "sidebar.newChat": "Nuevo chat",
"sidebar.collapse": "Ocultar barra lateral", "sidebar.collapse": "Ocultar barra lateral",
@@ -650,8 +658,12 @@ const fr: Dict = {
"login.forgot": "Clé d'accès oubliée ?", "login.forgot": "Clé d'accès oubliée ?",
"login.perk1": "Privé par conception — sans cookies ni suivi", "login.perk1": "Privé par conception — sans cookies ni suivi",
"login.perk2": "Vos propres modèles, en temps réel", "login.perk2": "Vos propres modèles, en temps réel",
"login.adminNote": "login.adminNotePre": "La clé admin vit dans le .env du serveur",
"La clé admin vit dans le .env du serveur ; les clés utilisateur sont émises par l'admin.", "login.adminNotePost": " ; les clés utilisateur sont émises par l'admin.",
"login.totpIntro": "Double facteur pour @{user} — entrez le code à 6 chiffres de votre app.",
"login.totpAria": "Code d'authentification",
"login.totpInvalid": "Code invalide",
"login.verify": "Vérifier",
"sidebar.newChat": "Nouveau chat", "sidebar.newChat": "Nouveau chat",
"sidebar.collapse": "Masquer la barre latérale", "sidebar.collapse": "Masquer la barre latérale",
@@ -943,8 +955,12 @@ const ru: Dict = {
"login.forgot": "Забыли ключ доступа?", "login.forgot": "Забыли ключ доступа?",
"login.perk1": "Приватность по умолчанию — без cookies и трекинга", "login.perk1": "Приватность по умолчанию — без cookies и трекинга",
"login.perk2": "Ваши модели, в реальном времени", "login.perk2": "Ваши модели, в реальном времени",
"login.adminNote": "login.adminNotePre": "Ключ админа живёт в .env сервера",
"Ключ админа живёт в .env сервера; ключи пользователей выдаёт админ.", "login.adminNotePost": "; ключи пользователей выдаёт админ.",
"login.totpIntro": "Двухфакторка для @{user} — введите 6-значный код из приложения.",
"login.totpAria": "Код аутентификатора",
"login.totpInvalid": "Неверный код",
"login.verify": "Проверить",
"sidebar.newChat": "Новый чат", "sidebar.newChat": "Новый чат",
"sidebar.collapse": "Скрыть панель", "sidebar.collapse": "Скрыть панель",
@@ -1225,8 +1241,12 @@ const it: Dict = {
"login.forgot": "Chiave dimenticata?", "login.forgot": "Chiave dimenticata?",
"login.perk1": "Privato per progettazione — niente cookie né traccianti", "login.perk1": "Privato per progettazione — niente cookie né traccianti",
"login.perk2": "I tuoi modelli, in tempo reale", "login.perk2": "I tuoi modelli, in tempo reale",
"login.adminNote": "login.adminNotePre": "La chiave admin vive nel .env del server",
"La chiave admin vive nel .env del server; le chiavi utente le emette l'admin.", "login.adminNotePost": "; le chiavi utente le emette l'admin.",
"login.totpIntro": "Doppio fattore per @{user} — inserisci il codice a 6 cifre dall'app.",
"login.totpAria": "Codice autenticatore",
"login.totpInvalid": "Codice non valido",
"login.verify": "Verifica",
"sidebar.newChat": "Nuova chat", "sidebar.newChat": "Nuova chat",
"sidebar.collapse": "Nascondi barra laterale", "sidebar.collapse": "Nascondi barra laterale",
@@ -1815,7 +1835,7 @@ export function t(
let s: string = dicts[current][key] ?? dicts.en[key] ?? key; let s: string = dicts[current][key] ?? dicts.en[key] ?? key;
if (vars) { if (vars) {
for (const [k, v] of Object.entries(vars)) for (const [k, v] of Object.entries(vars))
s = s.split(`{${k}}`).join(String(v)); s = s.replaceAll(`{${k}}`, String(v));
} }
return s; return s;
} }
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",