From e6b325eb53abd154373e8f503507513e53299b2f Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Thu, 23 Jul 2026 21:46:47 +0000 Subject: [PATCH] feat: per-user visibility (own/shared), anonymous generations, private sessions (generate+forget), NSFW model gating Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6 --- migrations/009_access_privacy_nsfw.sql | 18 +++++++++++++++ src/components/AdminApp.tsx | 32 ++++++++++++++++++++++++-- src/components/StudioApp.tsx | 17 +++++++++++++- src/lib/maintenance.ts | 22 +++++++++++++++--- src/lib/process.ts | 31 ++++++++++++++----------- src/pages/api/admin/models.ts | 4 ++++ src/pages/api/admin/settings.ts | 7 +++++- src/pages/api/admin/users.ts | 12 ++++++++-- src/pages/api/items/[id]/file.ts | 11 ++++++++- src/pages/api/items/index.ts | 12 +++++++--- src/pages/api/jobs/[id].ts | 9 +++++++- src/pages/api/jobs/index.ts | 23 ++++++++++++++---- src/pages/api/me.ts | 15 ++++++++++++ src/pages/api/models.ts | 8 ++++--- 14 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 migrations/009_access_privacy_nsfw.sql create mode 100644 src/pages/api/me.ts diff --git a/migrations/009_access_privacy_nsfw.sql b/migrations/009_access_privacy_nsfw.sql new file mode 100644 index 0000000..5aa54ad --- /dev/null +++ b/migrations/009_access_privacy_nsfw.sql @@ -0,0 +1,18 @@ +-- Zugriffsrechte, private Sessions, NSFW-Schalter. + +-- Sichtbarkeit der Bibliothek + Datenschutz. +ALTER TABLE settings ADD COLUMN IF NOT EXISTS library_visibility text NOT NULL DEFAULT 'own' + CHECK (library_visibility IN ('own','shared')); -- own = jede/r sieht nur eigene; shared = alle sehen alles +ALTER TABLE settings ADD COLUMN IF NOT EXISTS anonymous_generations bool NOT NULL DEFAULT false; +ALTER TABLE settings ADD COLUMN IF NOT EXISTS private_allowed bool NOT NULL DEFAULT false; +ALTER TABLE settings ADD COLUMN IF NOT EXISTS allow_nsfw bool NOT NULL DEFAULT false; + +-- Pro Nutzer: private Sessions erzwingen. +ALTER TABLE users ADD COLUMN IF NOT EXISTS private_forced bool NOT NULL DEFAULT false; + +-- Private Aufträge: nur erzeugen, danach vergessen (nicht in Bibliothek, kein Delivery/Backup, +-- kein gespeicherter Prompt, Quellen sofort gelöscht, Ergebnis nach kurzer Zeit entfernt). +ALTER TABLE jobs ADD COLUMN IF NOT EXISTS private bool NOT NULL DEFAULT false; + +-- Modelle als NSFW-fähig markieren (nur bei aktivem allow_nsfw wähl-/sichtbar). +ALTER TABLE models ADD COLUMN IF NOT EXISTS nsfw bool NOT NULL DEFAULT false; diff --git a/src/components/AdminApp.tsx b/src/components/AdminApp.tsx index d108b54..cd79766 100644 --- a/src/components/AdminApp.tsx +++ b/src/components/AdminApp.tsx @@ -75,6 +75,8 @@ export default function AdminApp() { cricut_sheet_cm: s.cricut_sheet_cm, monthly_budget: s.monthly_budget, n8n_webhook_url: s.n8n_webhook_url, keep_sources: !!s.keep_sources, make_thumbnails: s.make_thumbnails !== false, retention_days: s.retention_days, metadata_sidecar: !!s.metadata_sidecar, + library_visibility: s.library_visibility || 'own', anonymous_generations: !!s.anonymous_generations, + private_allowed: !!s.private_allowed, allow_nsfw: !!s.allow_nsfw, nas_enabled: !!s.nas_enabled, nas_host: s.nas_host, nas_protocol: s.nas_protocol, nas_port: s.nas_port, nas_user: s.nas_user, nas_base_path: s.nas_base_path, }; @@ -140,6 +142,10 @@ export default function AdminApp() { await fetch('/api/admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, password: pw }) }); notify('Passwort gesetzt.'); }; + const setUserPrivate = async (id: string, v: boolean) => { + await fetch('/api/admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, private_forced: v }) }); + load(); + }; const addUser = async () => { const username = prompt('Benutzername?'); if (!username) return; const password = prompt('Passwort?'); if (!password) return; @@ -306,6 +312,25 @@ export default function AdminApp() { +
+
Rechte & Datenschutz
+
+
+
+ + +
+
Admins sehen immer alles. „Nur eigene": jede/r sieht ausschließlich die selbst erzeugten Bilder.
+
+ + + +
+
+
@@ -313,9 +338,10 @@ export default function AdminApp() {
{models.map((m) => (
-
{m.label}{m.is_default && Standard}{m.supports_alpha && transparent}
{m.model_id}
+
{m.label}{m.is_default && Standard}{m.supports_alpha && transparent}{m.nsfw && NSFW}
{m.model_id}
{!m.is_default && } +
@@ -334,8 +360,9 @@ export default function AdminApp() {
{users.map((u) => (
-
{u.display_name || u.username} {u.username} · {u.role}
+
{u.display_name || u.username}{u.private_forced && privat} {u.username} · {u.role}
+
@@ -398,6 +425,7 @@ function AdminStyles() { .zeile b{font-size:14px;} .pille{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;background:var(--accent-bg);color:var(--accent);padding:2px 7px;border-radius:20px;margin-left:6px;} .pille.alpha{background:var(--paper);color:var(--soft);} + .pille.nsfw{background:var(--err);color:#fff;} .schalt{display:flex;gap:10px;align-items:flex-start;cursor:pointer;} .schalt input{margin-top:3px;width:16px;height:16px;accent-color:var(--accent);flex:0 0 auto;} .schalt b{display:block;font-size:14px;} diff --git a/src/components/StudioApp.tsx b/src/components/StudioApp.tsx index 8b39e2c..9fdd7fe 100644 --- a/src/components/StudioApp.tsx +++ b/src/components/StudioApp.tsx @@ -58,6 +58,8 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { const [targetId, setTargetId] = useState(''); const [targets, setTargets] = useState([]); const [presetSel, setPresetSel] = useState(''); + const [me, setMe] = useState({}); + const [priv, setPriv] = useState(false); const [models, setModels] = useState([]); const [modelKey, setModelKey] = useState(''); const [dragging, setDragging] = useState(false); @@ -119,6 +121,7 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { if (def) setModelKey(def.model_id); }).catch(() => {}); fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {}); + fetch('/api/me').then((r) => r.json()).then((j) => { setMe(j); if (j.private_forced) setPriv(true); }).catch(() => {}); loadPresets(); }, []); const loadPresets = () => fetch('/api/recipes').then((r) => r.json()).then((j) => setPresets(j.recipes || [])).catch(() => {}); @@ -208,7 +211,7 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { picdrop_gallery: (delivery !== 'library' && gallery.trim()) ? gallery.trim() : null, delivery_target_id: (delivery !== 'library' && targetId) ? targetId : null, }; - const body: any = { recipe, mode, delivery }; + const body: any = { recipe, mode, delivery, private: priv }; if (mode !== 'each') body.prompt_text = desc.trim(); body.sources = mode === 'generate' ? [] : ready.map((p) => ({ source_path: p.source_path, filename: p.name })); try { @@ -395,6 +398,14 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { )}
+ {(me.private_allowed || me.private_forced) && ( + + )} +