Privater Bereich /api/privat fuer Behoerdenfotos (d4rk-rp-planung#227 H1)

Polizeifotos aus dem Spiel duerfen nicht unter /f/ liegen, wo jeder sie mit
der Adresse bekommt. Neu:

- eigener Ordner DATA_DIR/privat neben files/ (fuer /f/ und /t/ durch
  Bauart unerreichbar), eigene Tabelle privat_media, eigene Vorschauen
- PUT/GET /api/privat/datei/<pfad>, ?vorschau=1; kein Ueberschreiben (409),
  kein Loeschweg; Cache-Control private, no-store
- Token-Fessel "privat": ein privater Token kommt an keinen oeffentlichen
  Weg (upload, Fivemanage, Liste), ein oeffentlicher an keinen privaten
- Dashboard: Kaestchen "nur privater Bereich" beim Anlegen und Bearbeiten,
  Marke in der Liste
- checkPathUnter: dieselbe Pfadregel fuer beide Ablagen
- tools/sichern.ps1 packt privat/ mit und zaehlt Dateien gegen Eintraege

Geprueft mit tools/privat-pruefen.mjs gegen einen Wegwerf-Container des
Abbilds: 21/21, mit Kontrollen (oeffentliches Bild MUSS unter /f/ kommen).
Dashboard gemessen (kopfloser Edge): 7/7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
D4rkst3randClaude Opus 5.5 committed 2026-10-16 21:36:16 +02:00
1 parent c8080fb2eb
commit 6630def7b2
14 files changed
+641 -15

No files matched your search

+5
View File
@@ -183,6 +183,8 @@ export type TokenRow = {
/** Erlaubte Arten, mit Komma. Leer = alle. */
arten: string
expires_at: number | null
/** 1 = nur privater Bereich (/api/privat), kein öffentlicher Upload. */
privat: number
abgelaufen: boolean
/** Wieviel mit DIESEM Token abgelegt wurde. */
dateien: number
@@ -199,6 +201,7 @@ export type NewToken = {
name: string
prefix: string
canDelete: boolean
privat: boolean
token: string
}
@@ -584,6 +587,7 @@ export const api = {
maxKante?: string | null
arten?: string[] | null
gueltigTage?: string | null
privat?: boolean
},
) => request<TokenRow & { geaendert: string[] }>(`/api/dash/tokens/${id}`, {
method: 'PATCH',
@@ -625,6 +629,7 @@ export const api = {
quotaMb?: string
arten?: string[]
gueltigTage?: string
privat?: boolean
}) => post<NewToken>('/api/dash/tokens', daten),
deleteToken: (id: number) => del<{ ok: true }>(`/api/dash/tokens/${id}`),
}
@@ -36,6 +36,7 @@ export function TokenBearbeiten({
const [name, setName] = useState(token.name)
const [prefix, setPrefix] = useState(token.prefix)
const [canDelete, setCanDelete] = useState(Boolean(token.can_delete))
const [privat, setPrivat] = useState(Boolean(token.privat))
const [maxMb, setMaxMb] = useState(
token.max_bytes ? String(Math.round((token.max_bytes / 1048576) * 10) / 10) : '',
)
@@ -65,6 +66,7 @@ export function TokenBearbeiten({
name: name.trim(),
prefix,
canDelete,
privat,
// Leer heißt ausdrücklich „keine Grenze" und nicht
// „nicht angefasst" — sonst ließe sich eine einmal
// gesetzte Grenze nie wieder loswerden.
@@ -148,6 +150,22 @@ export function TokenBearbeiten({
</span>
</label>
<label className="flex items-start gap-2 text-sm">
<input
type="checkbox"
checked={privat}
onChange={(e) => setPrivat(e.target.checked)}
className="mt-0.5 accent-akzent"
/>
<span>
nur <strong className="text-schrift">privater Bereich</strong>
<span className="block text-xs text-schrift-leise">
Wirkt sofort: angehakt kann der Token nicht mehr öffentlich
hochladen, abgehakt nicht mehr privat lesen.
</span>
</span>
</label>
<div>
<span className="mb-1 block text-xs font-medium text-schrift-leise">
Erlaubte Arten (nichts angekreuzt = alle)
+27
View File
@@ -14,6 +14,7 @@ export function Tokens() {
const [name, setName] = useState('')
const [prefix, setPrefix] = useState('')
const [darfLoeschen, setDarfLoeschen] = useState(false)
const [privat, setPrivat] = useState(false)
const [maxMb, setMaxMb] = useState('')
const [quotaMb, setQuotaMb] = useState('')
const [maxKante, setMaxKante] = useState('')
@@ -60,6 +61,7 @@ export function Tokens() {
maxKante: maxKante.trim(),
arten,
gueltigTage: gueltigTage.trim(),
privat,
}),
(t) => `Token "${t.name}" angelegt.`,
)
@@ -69,6 +71,7 @@ export function Tokens() {
setName('')
setPrefix('')
setDarfLoeschen(false)
setPrivat(false)
setMaxMb('')
setGueltigTage('')
setArten([])
@@ -205,6 +208,24 @@ export function Tokens() {
</span>
</label>
<label className="flex items-start gap-2 text-sm">
<input
type="checkbox"
checked={privat}
onChange={(e) => setPrivat(e.target.checked)}
className="mt-0.5 accent-akzent"
/>
<span>
nur privater Bereich
<span className="block text-xs text-schrift-leise">
Für Fotos, die nicht öffentlich sein dürfen (Behörden). Der Token
legt nur unter <code className="font-mono">/api/privat</code> ab
und liest nur dort — nicht unter /f/, ohne Liste, ohne Freigabe.
Öffentlich hochladen kann er dann nicht.
</span>
</span>
</label>
<div>
<Knopf
art="haupt"
@@ -274,6 +295,11 @@ export function Tokens() {
: '— (überall)'}
</td>
<td className="py-2">
{t.privat ? (
<span className="mr-1 rounded bg-akzent/15 px-1.5 py-0.5 text-xs text-schrift">
privat
</span>
) : null}
{t.can_delete ? (
<span className="rounded bg-warn/15 px-1.5 py-0.5 text-xs text-warn">
schreiben + löschen
@@ -411,6 +437,7 @@ export function Tokens() {
<p className="mt-3 text-xs text-schrift-leise">
Präfix: <code className="font-mono">{frisch.prefix || '—'}</code>{' '}
· Löschen: {frisch.canDelete ? 'erlaubt' : 'nicht erlaubt'}
{frisch.privat ? ' · nur privater Bereich' : ''}
</p>
</div>