CI / check (push) Has been cancelled
A self-hosted print studio: images at exact physical sizes, sheet layout with crop marks, tiling, 1:1 PDF. Everything else — image generation, delivery, share links, mail, Telegram — is an optional module that is off by default. This is the neutral public twin of a private tool. Compared to that original: - optional modules behind KLARBILD_MODULES; the default install is print-only - English interface with German as a full second language (1,038 strings); the English sentence is the translation key - no built-in accounts — first boot creates one admin from ADMIN_USERNAME / ADMIN_PASSWORD, or generates a password and logs it once - delivery and backup targets are generic SFTP/FTPS endpoints, off by default - English routes, English identifiers, English comments throughout - two compose files: print-only by default, everything in the full one - AGPL-3.0, CI (build, tests, type check, secret scan), CONTRIBUTING, SECURITY 124 tests green, type check clean, verified end to end against a live instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nh4A79bbqT9dcd2inzYUdg
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
import type { APIRoute } from 'astro';
|
|
import { query } from '../../../lib/db';
|
|
import { updateShare, revokeShare, unrevokeShare, deleteShare } from '../../../lib/shares';
|
|
import { has, moduleOff } from '../../../lib/modules';
|
|
|
|
export const prerender = false;
|
|
const json = (b: unknown, s = 200) =>
|
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
|
|
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
/** The most recent views of a link — for the report in the frontend. */
|
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
if (!has('share')) return moduleOff('share');
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
if (!UUID.test(params.id || '')) return json({ error: 'Unknown link.' }, 404);
|
|
const isAdmin = locals.user.role === 'admin';
|
|
const rows = await query<any>(
|
|
`SELECT v.kind, v.at, v.user_agent
|
|
FROM share_views v JOIN shares s ON s.id = v.share_id
|
|
WHERE v.share_id = $1 AND ($3::boolean OR s.created_by = $2)
|
|
ORDER BY v.at DESC LIMIT 100`, [params.id, locals.user.uid, isAdmin]);
|
|
return json({ visits: rows });
|
|
};
|
|
|
|
/** Change the title, expiry, passphrase or download permission; revoke or release. */
|
|
export const PATCH: APIRoute = async ({ params, request, locals }) => {
|
|
if (!has('share')) return moduleOff('share');
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
if (!UUID.test(params.id || '')) return json({ error: 'Unknown link.' }, 404);
|
|
const b = await request.json().catch(() => ({}));
|
|
const isAdmin = locals.user.role === 'admin';
|
|
|
|
if (b.action === 'revoke') {
|
|
return json({ ok: await revokeShare(params.id!, locals.user.uid, isAdmin) });
|
|
}
|
|
if (b.action === 'unrevoke') {
|
|
return json({ ok: await unrevokeShare(params.id!, locals.user.uid, isAdmin) });
|
|
}
|
|
const ok = await updateShare(params.id!, locals.user.uid, isAdmin, {
|
|
title: b.title, note: b.note, days: b.days,
|
|
allowDownload: b.allowDownload, password: b.password,
|
|
});
|
|
return ok ? json({ ok: true }) : json({ error: 'Unknown link.' }, 404);
|
|
};
|
|
|
|
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
if (!has('share')) return moduleOff('share');
|
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
|
if (!UUID.test(params.id || '')) return json({ error: 'Unknown link.' }, 404);
|
|
const ok = await deleteShare(params.id!, locals.user.uid, locals.user.role === 'admin');
|
|
return ok ? json({ ok: true }) : json({ error: 'Unknown link.' }, 404);
|
|
};
|