Deleting an image in the library removed the row and the result file on the spot. One slip and a result was gone, including what it had cost to generate. Now a delete moves the image into the trash: - It disappears from the library, from every share link, from ZIPs, from deliveries, the backup mirror, print sources and the print history — and comes back unchanged when restored. - The toast offers "Undo" for seven seconds. A trash view lists what is in there with the days left, restore, delete for good, empty. - Maintenance removes images for good after `settings.trash_days` (default 30, set in Admin -> Storage). 0 switches the trash off. - An image trashed while it is still queued is skipped by the worker instead of being generated — and billed — for nobody. - Deleting for good removes only files no other image still uses. An alternative version shares its source with the original; before, the retention cleanup could take the source of the other one with it. This includes combine sources, which live in jsonb, not in an array. While going through every query that touches images, it turned out that only the file route checked who may see an image. Renaming, deleting, retrying, generating an alternative, reusing and zipping took any id from any signed-in account. On a single-user install that changes nothing; on a family install one account could delete another's images. One rule now lives in src/lib/itemaccess.ts: read your own (or everything in a shared library, never a private session), change only your own; admins all. A refused id answers 404 like a missing one. Also: a favicon and an iPhone home-screen icon. There was none, so the tab showed a blank page and /favicon.ico bounced to the login. Verified against a real Postgres with two accounts and real files on disk: 35 end-to-end checks (access, share links, restore, purge with shared sources, expiry through maintenance, trash switched off). In the browser, English and German: delete, undo, trash view, restore, no horizontal scroll at 390 px. Unit tests: 130 (4 new).
47 lines
2.3 KiB
TypeScript
47 lines
2.3 KiB
TypeScript
import type { APIRoute } from 'astro';
|
|
import { one, query } from '../../../lib/db';
|
|
|
|
export const prerender = false;
|
|
const json = (b: unknown, s = 200) =>
|
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
|
|
|
export const GET: APIRoute = async ({ locals }) => {
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
const rows = await query(`SELECT f.*, (SELECT count(*) FROM items WHERE folder_id=f.id AND deleted_at IS NULL)::int AS count
|
|
FROM folders f ORDER BY f.name`);
|
|
return json({ folders: rows });
|
|
};
|
|
|
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
const b = await request.json();
|
|
if (!b.name?.trim()) return json({ error: 'Name is missing.' }, 400);
|
|
const row = await one(`INSERT INTO folders (name, delivery_folder) VALUES ($1,$2) RETURNING *`,
|
|
[b.name.trim(), b.delivery_folder || null]);
|
|
return json({ folder: row });
|
|
};
|
|
|
|
// Rename a folder / change the folder on the target. Body { id, name?, delivery_folder? }
|
|
export const PATCH: APIRoute = async ({ request, locals }) => {
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
const b = await request.json();
|
|
if (!b.id) return json({ error: 'No id.' }, 400);
|
|
const sets: string[] = []; const args: any[] = [];
|
|
if (typeof b.name === 'string' && b.name.trim()) { args.push(b.name.trim()); sets.push(`name=$${args.length}`); }
|
|
if ('delivery_folder' in b) { args.push(b.delivery_folder?.trim() || null); sets.push(`delivery_folder=$${args.length}`); }
|
|
if (!sets.length) return json({ error: 'Nothing to change.' }, 400);
|
|
args.push(b.id);
|
|
const row = await one(`UPDATE folders SET ${sets.join(',')} WHERE id=$${args.length} RETURNING *`, args);
|
|
return json({ folder: row });
|
|
};
|
|
|
|
// Delete a folder (the images stay, they only lose the assignment). ?id=…
|
|
export const DELETE: APIRoute = async ({ request, url, locals }) => {
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
let id = url.searchParams.get('id');
|
|
if (!id) { try { id = (await request.json())?.id; } catch { /* no matter */ } }
|
|
if (!id) return json({ error: 'No id.' }, 400);
|
|
await query('DELETE FROM folders WHERE id=$1', [id]);
|
|
return json({ ok: true });
|
|
};
|