feat: per-user visibility (own/shared), anonymous generations, private sessions (generate+forget), NSFW model gating
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -31,6 +31,10 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
await query('UPDATE models SET is_default=false'); await query('UPDATE models SET is_default=true WHERE id=$1', [b.id]);
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (b.action === 'nsfw' && b.id) {
|
||||
await query('UPDATE models SET nsfw = NOT nsfw WHERE id=$1', [b.id]);
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (!b.model_id || !b.label) return json({ error: 'model_id + label nötig.' }, 400);
|
||||
const existing = await one<{ id: string }>('SELECT id FROM models WHERE model_id=$1', [b.model_id]);
|
||||
if (existing) {
|
||||
|
||||
@@ -24,6 +24,9 @@ export const GET: APIRoute = async () => {
|
||||
// Speicherverwaltung
|
||||
keep_sources: s.keep_sources, make_thumbnails: s.make_thumbnails, retention_days: s.retention_days,
|
||||
metadata_sidecar: s.metadata_sidecar, api_token: s.api_token,
|
||||
// Rechte / Datenschutz / NSFW
|
||||
library_visibility: s.library_visibility, anonymous_generations: s.anonymous_generations,
|
||||
private_allowed: s.private_allowed, allow_nsfw: s.allow_nsfw,
|
||||
// NAS-Sicherung
|
||||
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, nas_password_set: !!s.nas_password_enc,
|
||||
@@ -39,10 +42,12 @@ export const PATCH: APIRoute = async ({ request }) => {
|
||||
if (b.picdrop_password) set('picdrop_password_enc', encrypt(String(b.picdrop_password)));
|
||||
if (b.nas_password) set('nas_password_enc', encrypt(String(b.nas_password)));
|
||||
if (b.generate_api_token) set('api_token', randomToken());
|
||||
const boolCols = ['keep_sources', 'make_thumbnails', 'nas_enabled', 'metadata_sidecar'];
|
||||
const boolCols = ['keep_sources', 'make_thumbnails', 'nas_enabled', 'metadata_sidecar',
|
||||
'anonymous_generations', 'private_allowed', 'allow_nsfw'];
|
||||
for (const col of ['picdrop_host', 'picdrop_protocol', 'picdrop_port', 'picdrop_user', 'picdrop_base_path',
|
||||
'picdrop_default_gallery', 'default_dpi', 'default_crop_mode', 'concurrency', 'cricut_sheet_cm', 'monthly_budget', 'n8n_webhook_url',
|
||||
'keep_sources', 'make_thumbnails', 'retention_days', 'metadata_sidecar', 'api_token',
|
||||
'library_visibility', 'anonymous_generations', 'private_allowed', 'allow_nsfw',
|
||||
'nas_enabled', 'nas_host', 'nas_protocol', 'nas_port', 'nas_user', 'nas_base_path']) {
|
||||
if (col in b) {
|
||||
const raw = b[col];
|
||||
|
||||
@@ -7,17 +7,25 @@ const json = (b: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const users = await query('SELECT id, username, role, display_name, created_at FROM users ORDER BY username');
|
||||
const users = await query('SELECT id, username, role, display_name, private_forced, created_at FROM users ORDER BY username');
|
||||
return json({ users });
|
||||
};
|
||||
|
||||
// POST: neuen Nutzer anlegen ODER Passwort setzen (bei vorhandenem username/id).
|
||||
// POST: neuen Nutzer anlegen ODER Passwort/Attribute setzen (bei vorhandenem username/id).
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const b = await request.json();
|
||||
if (b.id && b.password) {
|
||||
await query('UPDATE users SET password_hash=$2 WHERE id=$1', [b.id, await hashPassword(b.password)]);
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (b.id && 'private_forced' in b) {
|
||||
await query('UPDATE users SET private_forced=$2 WHERE id=$1', [b.id, !!b.private_forced]);
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (b.id && b.role) {
|
||||
await query(`UPDATE users SET role=$2 WHERE id=$1`, [b.id, b.role === 'admin' ? 'admin' : 'user']);
|
||||
return json({ ok: true });
|
||||
}
|
||||
if (!b.username || !b.password) return json({ error: 'username + password nötig.' }, 400);
|
||||
const exists = await one('SELECT id FROM users WHERE username=$1', [b.username.trim()]);
|
||||
if (exists) return json({ error: 'Benutzername vergeben.' }, 400);
|
||||
|
||||
@@ -11,7 +11,16 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
||||
const wantThumb = q.get('thumb') === '1';
|
||||
const wantSrc = q.get('src') === '1';
|
||||
const item = await one<any>(
|
||||
`SELECT source_path, source_paths, result_path, thumb_path, filename, has_alpha FROM items WHERE id=$1`, [params.id]);
|
||||
`SELECT i.source_path, i.source_paths, i.result_path, i.thumb_path, i.filename, i.has_alpha,
|
||||
j.created_by, j.private FROM items i JOIN jobs j ON j.id=i.job_id WHERE i.id=$1`, [params.id]);
|
||||
if (item) {
|
||||
const isAdmin = locals.user.role === 'admin';
|
||||
const own = item.created_by === locals.user.uid;
|
||||
if (!isAdmin && !own) {
|
||||
const s = await one<{ library_visibility: string }>('SELECT library_visibility FROM settings WHERE id=1');
|
||||
if (item.private || s?.library_visibility !== 'shared') return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
}
|
||||
// Quelle: Einzelquelle, sonst erste Kombinier-Quelle (für Vorher/Nachher).
|
||||
const firstSource = item?.source_path || (Array.isArray(item?.source_paths) ? item.source_paths[0] : null);
|
||||
const path = wantSrc ? firstSource
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { query } from '../../../lib/db';
|
||||
import { one, query } from '../../../lib/db';
|
||||
|
||||
export const prerender = false;
|
||||
const json = (b: unknown, s = 200) =>
|
||||
@@ -9,9 +9,14 @@ const json = (b: unknown, s = 200) =>
|
||||
export const GET: APIRoute = async ({ url, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const q = url.searchParams;
|
||||
const where: string[] = [`i.status='done'`];
|
||||
const s = await one<any>('SELECT library_visibility, anonymous_generations FROM settings WHERE id=1');
|
||||
const isAdmin = locals.user.role === 'admin';
|
||||
const shared = s?.library_visibility === 'shared';
|
||||
// Private Aufträge nie in der Bibliothek; Sichtbarkeit nach Einstellung.
|
||||
const where: string[] = [`i.status='done'`, `j.private=false`];
|
||||
const args: any[] = [];
|
||||
const add = (cond: string, val: any) => { args.push(val); where.push(cond.replace('?', `$${args.length}`)); };
|
||||
if (!isAdmin && !shared) add('j.created_by = ?', locals.user.uid);
|
||||
|
||||
if (q.get('folder')) add('i.folder_id = ?', q.get('folder'));
|
||||
if (q.get('delivery')) add('i.delivery_status = ?', q.get('delivery'));
|
||||
@@ -24,7 +29,8 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
||||
|
||||
const rows = await query(
|
||||
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.thumb_path, i.folder_id,
|
||||
i.delivery_status, i.nas_status, i.prompt_used, i.created_at, i.model_used, i.variant_of, j.mode, u.display_name AS by_name,
|
||||
i.delivery_status, i.nas_status, i.prompt_used, i.created_at, i.model_used, i.variant_of, j.mode,
|
||||
${(s?.anonymous_generations && !isAdmin) ? 'NULL' : 'u.display_name'} AS by_name,
|
||||
j.recipe_snapshot->'tasks' AS tasks
|
||||
FROM items i
|
||||
JOIN jobs j ON j.id = i.job_id
|
||||
|
||||
@@ -7,8 +7,15 @@ const json = (b: unknown, s = 200) =>
|
||||
|
||||
export const GET: APIRoute = async ({ params, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const job = await one('SELECT * FROM jobs WHERE id=$1', [params.id]);
|
||||
const job = await one<any>('SELECT * FROM jobs WHERE id=$1', [params.id]);
|
||||
if (!job) return json({ error: 'Nicht gefunden.' }, 404);
|
||||
// Zugriff: eigener Auftrag, Admin, oder shared (nicht privat).
|
||||
const isAdmin = locals.user.role === 'admin';
|
||||
const own = job.created_by === locals.user.uid;
|
||||
if (!isAdmin && !own) {
|
||||
const s = await one<{ library_visibility: string }>('SELECT library_visibility FROM settings WHERE id=1');
|
||||
if (job.private || s?.library_visibility !== 'shared') return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
const items = await query(
|
||||
`SELECT id, position, status, filename, output_px, has_alpha, error_message,
|
||||
result_path, delivery_status, cost FROM items WHERE job_id=$1 ORDER BY position`,
|
||||
|
||||
@@ -8,9 +8,16 @@ const json = (b: unknown, s = 200) =>
|
||||
|
||||
export const GET: APIRoute = async ({ locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const s = await one<any>('SELECT library_visibility, anonymous_generations FROM settings WHERE id=1');
|
||||
const isAdmin = locals.user.role === 'admin';
|
||||
const shared = s?.library_visibility === 'shared';
|
||||
const where: string[] = ['j.private=false']; const args: any[] = [];
|
||||
if (!isAdmin && !shared) { args.push(locals.user.uid); where.push(`j.created_by=$${args.length}`); }
|
||||
const nameCol = (s?.anonymous_generations && !isAdmin) ? 'NULL' : 'u.display_name';
|
||||
const rows = await query(
|
||||
`SELECT j.*, u.display_name AS by_name FROM jobs j
|
||||
LEFT JOIN users u ON u.id=j.created_by ORDER BY j.created_at DESC LIMIT 50`);
|
||||
`SELECT j.*, ${nameCol} AS by_name FROM jobs j
|
||||
LEFT JOIN users u ON u.id=j.created_by
|
||||
WHERE ${where.join(' AND ')} ORDER BY j.created_at DESC LIMIT 50`, args);
|
||||
return json({ jobs: rows });
|
||||
};
|
||||
|
||||
@@ -52,11 +59,17 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
delivery_target_id: snapshot.delivery_target_id ?? null,
|
||||
};
|
||||
|
||||
// Private Session: nur wenn global erlaubt oder für den Nutzer erzwungen.
|
||||
const acc = await one<any>(`SELECT s.private_allowed, u.private_forced
|
||||
FROM settings s, users u WHERE s.id=1 AND u.id=$1`, [locals.user.uid]);
|
||||
const isPrivate = !!(acc?.private_forced || (acc?.private_allowed && b.private));
|
||||
if (isPrivate) snap.delivery = 'library'; // kein externes Delivery bei privaten Aufträgen
|
||||
|
||||
const total = (mode === 'each') ? sources.length : 1;
|
||||
const job = await one<{ id: string }>(
|
||||
`INSERT INTO jobs (created_by, origin, mode, recipe_snapshot, status, total)
|
||||
VALUES ($1,$2,$3,$4,'queued',$5) RETURNING id`,
|
||||
[locals.user.uid, b.origin || 'web', mode, JSON.stringify(snap), total]);
|
||||
`INSERT INTO jobs (created_by, origin, mode, recipe_snapshot, status, total, private)
|
||||
VALUES ($1,$2,$3,$4,'queued',$5,$6) RETURNING id`,
|
||||
[locals.user.uid, b.origin || 'web', mode, JSON.stringify(snap), total, isPrivate]);
|
||||
|
||||
if (mode === 'each') {
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { one } from '../../lib/db';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const GET: APIRoute = async ({ locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const s = await one<{ private_allowed: boolean }>('SELECT private_allowed FROM settings WHERE id=1');
|
||||
const u = await one<{ private_forced: boolean }>('SELECT private_forced FROM users WHERE id=$1', [locals.user.uid]);
|
||||
return new Response(JSON.stringify({
|
||||
role: locals.user.role,
|
||||
private_allowed: !!s?.private_allowed,
|
||||
private_forced: !!u?.private_forced,
|
||||
}), { headers: { 'Content-Type': 'application/json' } });
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { query } from '../../lib/db';
|
||||
import { one, query } from '../../lib/db';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// Aktive Modelle mit freundlichen Namen (für Studio-Auswahl + Ergebnis-Anzeige).
|
||||
// NSFW-fähige Modelle nur, wenn allow_nsfw in den Einstellungen aktiv ist.
|
||||
export const GET: APIRoute = async ({ locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const s = await one<{ allow_nsfw: boolean }>('SELECT allow_nsfw FROM settings WHERE id=1');
|
||||
const models = await query(
|
||||
`SELECT model_id, label, description, is_default, supports_alpha FROM models
|
||||
WHERE active ORDER BY sort, label`);
|
||||
`SELECT model_id, label, description, is_default, supports_alpha, nsfw FROM models
|
||||
WHERE active AND (nsfw=false OR $1=true) ORDER BY sort, label`, [!!s?.allow_nsfw]);
|
||||
return new Response(JSON.stringify({ models }), { headers: { 'Content-Type': 'application/json' } });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user