Compare commits

...
2 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 e173c3bf7b docs: Stand nach dem Smoke-Test, und der Stolperstein beim lokalen Start
Der Durchstich steht: anmelden, Token anlegen, Bild hochladen, unter der
zurueckgegebenen URL abrufen -- Byte fuer Byte identisch, auch unter dem
Dateihost.

server/.env.example ist die Datei, die .gitignore ohnehin schon erwartet
hat. Wichtigster Eintrag darin ist FILES_HOST: ohne ihn nimmt der Dienst
den Host aus PUBLIC_URL, lokal also localhost. Dann gilt jede Anfrage als
Anfrage an den Dateiwirt, und dort gibt es absichtlich weder Dashboard
noch API -- /health antwortet 404, die Anmeldung 405 "hier gibt es nur
Dateien". Nichts ist kaputt, der Dienst tut genau das, was er soll, nur
eben unter dem einen Namen, den man lokal hat. In Produktion sind es zwei
verschiedene Namen und der Standard stimmt.

Das dev-Skript liest die .env jetzt mit --env-file (funktioniert unter
tsx, nachgeprueft).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:39:46 +02:00
D4rkst3randClaude Opus 5 cfd0f0747a fix: sechs Befunde aus dem ersten Start des Servers
Das Grundgeruest war typgeprueft, aber nie gelaufen. Beim ersten Start
haben sich sechs Dinge gezeigt, alle nachgemessen statt vermutet:

1. Die Token-Pruefung hing an '*' und galt damit auch fuer /api/dash/*,
   das daneben liegt. Das Dashboard bekam "Token fehlt oder ist
   unbekannt" auf die Anmeldung, obwohl es nie einen Token haben kann.
   Sie haengt jetzt an den drei eigenen Pfaden.

2. DELETE /api/media/... und GET /api/exists/... sahen am Ziel vorbei.
   c.req.path traegt den Einhaengepunkt mit, das replace(/^\/media\//)
   schnitt ihn nicht weg -- aus vehicles/adder.png wurde
   api/media/vehicles/adder.png. Loeschen fand nie etwas, exists meldete
   immer false. Jetzt :pfad{.+}; Hono liefert den Parameter fertig
   dekodiert (an einer Probe gemessen), ein zweites decodeURIComponent
   waere eine Dekodierung zu viel.

3. Verzeichnisdurchstieg in der SPA-Rueckfallroute: GET /..%5Cpackage.json
   hat unter Windows die Datei ausgeliefert. Hono reicht %5C durch,
   path.join behandelt den Backslash dort als Trenner, und eine
   Eindaemmung gab es nicht. Unter Linux traegt genau dieser Angriff
   nicht -- Glueck, keine Abwehr. Jetzt dieselbe resolve-Pruefung wie in
   storage.ts.

4. Die Auskunft "Oberflaeche ist nicht gebaut" war unerreichbar.
   createReadStream meldet eine fehlende Datei asynchron, das try/catch
   darum fing nichts. Ergebnis war ein leerer 200 samt ENOENT im Log.

5. CSS und JS kamen als application/octet-stream -- die MIME-Tabelle
   kennt nur Medientypen, das Dashboard haette weder Stylesheet noch
   Modul geladen. Die Oberflaeche bekommt eine eigene Tabelle: in der
   geteilten fehlt html mit Absicht, sonst koennte jeder mit einem
   Upload-Token eine Seite unter fivecdn.d4rkst3r.de veroeffentlichen.

6. Kaputtes JSON endete als nackter "Internal Server Error".
   Ausgerechnet das -- ein 500 ohne ein Wort dazu ist der Fehler, wegen
   dem wir hier neu bauen. Jetzt 400 mit Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:39:33 +02:00
6 changed files with 248 additions and 48 deletions
+66 -5
View File
@@ -63,8 +63,6 @@ server/src/routes/dash.ts die API fürs Dashboard
server/src/index.ts Host-Weiche, Dateiauslieferung, Start server/src/index.ts Host-Weiche, Dateiauslieferung, Start
``` ```
Typprüfung läuft durch. **Noch nicht gestartet und nicht getestet.**
Die API für Skripte: Die API für Skripte:
``` ```
@@ -86,10 +84,73 @@ Die API fürs Dashboard: Anmeldung, Passwortwechsel, Medienliste mit Suche und
Paginierung, Einzel- und Sammellöschung, Statistik nach Ordnern, Paginierung, Einzel- und Sammellöschung, Statistik nach Ordnern,
Token-Verwaltung. Token-Verwaltung.
### ⬜ Als Nächstes ### ✅ Fertig — Schritt 1, der Smoke-Test
**1 · Smoke-Test.** Server lokal starten, Token anlegen, ein Bild hoch- und Gestartet, durchgemessen, repariert. Der Durchstich steht: anmelden → Token
wieder abrufen. Vor Docker, vor allem anderen. anlegen → Bild hochladen → unter der zurückgegebenen URL abrufen, Byte für Byte
identisch, auch unter dem Dateihost.
**So läuft er lokal:**
```bash
cp server/.env.example server/.env
```
```bash
npm --prefix server ci && npm --prefix server run dev
```
> **Der Stolperstein beim lokalen Start:** ohne `FILES_HOST` nimmt der Dienst
> den Host aus `PUBLIC_URL` — lokal also `localhost`. Dann gilt *jede* Anfrage
> als Anfrage an den Dateiwirt, und dort gibt es absichtlich weder Dashboard
> noch API: `/health` antwortet 404, die Anmeldung 405 „hier gibt es nur
> Dateien". Nichts ist kaputt. In Produktion sind es zwei verschiedene Namen
> und der Standard stimmt; lokal gehört ein Name hin, den niemand aufruft.
> Steht samt Begründung in `server/.env.example`.
**Was der Test gefunden hat** — alles im Grundgerüst, alles repariert:
1. **Die Token-Prüfung hing an `'*'`** und galt damit auch für `/api/dash/*`,
das daneben liegt. Das Dashboard bekam „Token fehlt oder ist unbekannt" auf
die Anmeldung, obwohl es nie einen Token haben kann. Jetzt hängt sie an den
drei eigenen Pfaden.
2. **`DELETE /api/media/…` und `GET /api/exists/…` sahen am Ziel vorbei.**
`c.req.path` trägt den Einhängepunkt mit, das `replace(/^\/media\//)` schnitt
ihn nicht weg — aus `vehicles/adder.png` wurde `api/media/vehicles/adder.png`.
Löschen fand nie etwas, `exists` meldete immer `false`. Jetzt `:pfad{.+}`;
Hono liefert den Parameter fertig dekodiert (nachgemessen), ein zweites
`decodeURIComponent` wäre eine Dekodierung zu viel gewesen.
3. **Verzeichnisdurchstieg in der SPA-Rückfallroute.**
`GET /..%5Cpackage.json` hat unter Windows die Datei ausgeliefert: Hono
reicht `%5C` durch, `path.join` behandelt den Backslash dort als Trenner,
und eine Eindämmung gab es nicht. Unter Linux trägt genau dieser Angriff
nicht — Glück, keine Abwehr. Jetzt dieselbe `resolve`-Prüfung wie in
`storage.ts`.
4. **Die Auskunft „Oberfläche ist nicht gebaut" war unerreichbar.**
`createReadStream` meldet eine fehlende Datei *asynchron*; das `try/catch`
darum fing nichts. Ergebnis war ein leerer 200 samt ENOENT im Log. Jetzt
wird vorher nachgesehen.
5. **CSS und JS kamen als `application/octet-stream`** — die MIME-Tabelle kennt
nur Medientypen. Das Dashboard hätte weder Stylesheet noch Modul geladen.
Die Oberfläche hat jetzt eine **eigene** Tabelle: in der geteilten fehlt
`html` mit Absicht, sonst könnte jeder mit einem Upload-Token eine Seite
unter `fivecdn.d4rkst3r.de` veröffentlichen. Nachgemessen: eine hochgeladene
`.html` kommt weiterhin als `octet-stream` mit `nosniff`.
6. **Kaputtes JSON endete als nackter „Internal Server Error".** Ausgerechnet
das — ein 500 ohne ein Wort dazu ist der Fehler, wegen dem wir hier neu
bauen. Jetzt 400 mit Text.
**Was sonst nachgemessen wurde und trägt:** alle drei Rumpfformen (multipart,
Base64, rohe Bytes) liefern denselben SHA-256, auch mit `data:`-Vorspann · der
Hash-Pfad ohne `X-Path` · Präfix-Fessel und `can_delete` je Token · `..`,
versteckte Dateien und leerer Rumpf werden abgelehnt · `X-Overwrite: false`
gibt 409, Überschreiben `replaced: true` · 413 vor dem Einlesen · ETag/304,
HEAD, Bereichsanfragen und 416 · unter dem Dateihost gibt es weder API noch
`/f/`-Präfix · Medienliste, Suche, Statistik nach Ordnern.
Nicht getestet: Docker, das Dashboard (gibt es noch nicht), und echte Last.
### ⬜ Als Nächstes
**2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche **2 · Dockerfile und Compose.** Ein Abbild, das Server und gebaute Oberfläche
ausliefert; ein Volume für `data/`; ein Port für NPM. In Portainer aus diesem ausliefert; ein Volume für `data/`; ein Port für NPM. In Portainer aus diesem
+43
View File
@@ -0,0 +1,43 @@
# Kopieren nach server/.env, dann `npm run dev`.
#
# Ohne PUBLIC_URL und ADMIN_PASSWORD startet der Dienst nicht — das ist
# Absicht, siehe config.ts.
# Unter welcher Adresse die Dateien oeffentlich stehen. Daraus wird die URL
# gebaut, die nach dem Upload zurueckkommt. Ohne abschliessenden Schraegstrich.
PUBLIC_URL=http://localhost:8080/f
# DER STOLPERSTEIN BEIM LOKALEN START.
#
# Ohne diese Zeile nimmt der Dienst den Host aus PUBLIC_URL — lokal also
# "localhost". Dann gilt jede Anfrage als Anfrage an den Dateiwirt, und dort
# gibt es absichtlich weder Dashboard noch API: /health antwortet 404, die
# Anmeldung 405 "hier gibt es nur Dateien". Nichts ist kaputt, der Dienst tut
# genau das, was er soll — nur eben unter dem einen Namen, den man lokal hat.
#
# In Produktion sind es zwei verschiedene Namen und der Standard stimmt:
# PUBLIC_URL=https://fivecdn.d4rkst3r.de
# FILES_HOST=fivecdn.d4rkst3r.de (Dashboard: fivemanage.d4rkst3r.de)
#
# Lokal deshalb ein Name, den niemand aufruft — dann bleibt localhost fuer
# Dashboard und API frei. Zum Testen des Dateiwirts:
# curl -H "Host: files.localhost" http://localhost:8080/vehicles/adder.webp
FILES_HOST=files.localhost
# Wird beim ERSTEN Start angelegt. Ein spaeter geaendertes Passwort setzt das
# bestehende nicht zurueck.
ADMIN_PASSWORD=bitte-aendern
ADMIN_USERNAME=admin
PORT=8080
# Ein Ordner fuer Bilder UND media.db — eine Sicherung ist damit ein cp -a.
# Relativ zum Arbeitsverzeichnis; im Abbild spaeter /data.
DATA_DIR=./data
# Groesste erlaubte Datei in MB. Ein freigestelltes Fahrzeugbild wiegt rund
# 200 KB.
MAX_UPLOAD_MB=64
# Wie lange eine Anmeldung im Dashboard haelt.
SESSION_DAYS=30
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch --env-file=.env src/index.ts",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"start": "node dist/index.js" "start": "node dist/index.js"
}, },
+86 -23
View File
@@ -18,7 +18,7 @@
import { createReadStream } from 'node:fs' import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises' import { stat } from 'node:fs/promises'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { join } from 'node:path' import { join, resolve, sep } from 'node:path'
import { serve } from '@hono/node-server' import { serve } from '@hono/node-server'
import { Hono } from 'hono' import { Hono } from 'hono'
import { logger } from 'hono/logger' import { logger } from 'hono/logger'
@@ -120,38 +120,101 @@ app.get('/f/*', (c) => serveFile(c, c.req.path.slice(3)))
// index.html — sonst endet ein Neuladen auf /tokens im 404 statt im Router. // index.html — sonst endet ein Neuladen auf /tokens im 404 statt im Router.
// fileURLToPath und nicht .pathname: unter Windows liefert letzteres // fileURLToPath und nicht .pathname: unter Windows liefert letzteres
// "/C:/..." und jedes stat() darauf schlaegt fehl. // "/C:/..." und jedes stat() darauf schlaegt fehl.
const webDir = fileURLToPath(new URL('../web/', import.meta.url)) const webDir = resolve(fileURLToPath(new URL('../web/', import.meta.url)))
/** Der absolute Pfad zu einer Datei der Oberflaeche — oder null, wenn er aus
* dem Ordner herauszeigt.
*
* Dieselbe Guertel-und-Hosentraeger-Regel wie in storage.ts, und aus demselben
* Grund: der Pfad kommt vom Aufrufer. Ohne die Pruefung hat
* `GET /..%5Cpackage.json` unter Windows die Datei ausgeliefert — Hono reicht
* %5C unveraendert durch, und path.join behandelt den Backslash dort als
* Trenner. Unter Linux traegt derselbe Angriff nicht, aber das ist Glueck und
* keine Abwehr. */
function webFile(candidate: string): string | null {
const target = resolve(webDir, decodeSafely(candidate).replace(/^[/\\]+/, ''))
if (target !== webDir && !target.startsWith(webDir + sep)) return null
return target
}
/** %5C und Konsorten aufloesen, bevor geprueft wird — sonst prueft die
* Eindaemmung eine andere Zeichenkette als die, die spaeter im Dateisystem
* landet. Ein kaputtes Prozentzeichen ist kein Grund abzustuerzen. */
function decodeSafely(raw: string): string {
try {
return decodeURIComponent(raw)
} catch {
return raw
}
}
/** Die Dateitypen der Oberflaeche — bewusst eine EIGENE Tabelle und nicht die
* aus storage.ts.
*
* Dort fehlen html, css und js mit gutem Grund: eine hochgeladene .html wird
* unter dem Dateihost als application/octet-stream ausgeliefert und damit
* heruntergeladen statt ausgefuehrt. Stuende html in der geteilten Tabelle,
* koennte jeder mit einem Upload-Token eine Seite unter fivecdn.d4rkst3r.de
* veroeffentlichen. Die gebaute Oberflaeche liegt dagegen im Abbild und kommt
* von uns — hier ist der richtige Typ noetig, sonst laedt der Browser weder
* Stylesheet noch Modul. */
const WEB_MIME: Record<string, string> = {
html: 'text/html; charset=utf-8',
css: 'text/css; charset=utf-8',
js: 'text/javascript; charset=utf-8',
mjs: 'text/javascript; charset=utf-8',
map: 'application/json; charset=utf-8',
ico: 'image/x-icon',
woff: 'font/woff',
woff2: 'font/woff2',
ttf: 'font/ttf',
}
const webMimeFor = (path: string) =>
WEB_MIME[path.split('.').pop()?.toLowerCase() ?? ''] ?? mimeFor(path)
app.get('*', async (c) => { app.get('*', async (c) => {
if (c.req.path.startsWith('/api/')) return c.json({ error: 'unbekannt' }, 404) if (c.req.path.startsWith('/api/')) return c.json({ error: 'unbekannt' }, 404)
const candidate = c.req.path === '/' ? '/index.html' : c.req.path const candidate = c.req.path === '/' ? '/index.html' : c.req.path
const target = webFile(candidate)
if (target) {
try {
const info = await stat(target)
if (info.isFile()) {
return c.body(createReadStream(target) as any, 200, {
'Content-Type': webMimeFor(candidate),
'Cache-Control': candidate.includes('/assets/')
? 'public, max-age=31536000, immutable'
: 'no-cache',
})
}
} catch {
/* faellt unten auf index.html */
}
}
// Erst nachsehen, dann streamen. createReadStream meldet eine fehlende
// Datei ASYNCHRON ueber ein 'error'-Ereignis — ein try/catch darum faengt
// nichts. Vorher stand hier genau das, und das Ergebnis war bei ungebauter
// Oberflaeche ein leerer 200 samt ENOENT im Log statt der Auskunft unten.
const index = join(webDir, 'index.html')
try { try {
const target = join(webDir, candidate.replace(/^\//, '')) if ((await stat(index)).isFile()) {
const info = await stat(target) return c.body(createReadStream(index) as any, 200, {
if (info.isFile()) { 'Content-Type': 'text/html; charset=utf-8',
return c.body(createReadStream(target) as any, 200, { 'Cache-Control': 'no-cache',
'Content-Type': mimeFor(candidate),
'Cache-Control': candidate.includes('/assets/')
? 'public, max-age=31536000, immutable'
: 'no-cache',
}) })
} }
} catch { } catch {
/* faellt unten auf index.html */ /* faellt auf die Auskunft unten */
} }
try { return c.text(
return c.body(createReadStream(join(webDir, 'index.html')) as any, 200, { 'Die Oberflaeche ist nicht gebaut. Der Dienst laeuft trotzdem — ' +
'Content-Type': 'text/html; charset=utf-8', 'die API steht unter /api.',
'Cache-Control': 'no-cache', 200,
}) )
} catch {
return c.text(
'Die Oberflaeche ist nicht gebaut. Der Dienst laeuft trotzdem — ' +
'die API steht unter /api.',
200,
)
}
}) })
// --------------------------------------------------------------------- Start // --------------------------------------------------------------------- Start
+29 -11
View File
@@ -6,6 +6,7 @@
// Rueckmeldung wie ein kaputter Knopf aussieht. // Rueckmeldung wie ein kaputter Knopf aussieht.
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Context } from 'hono'
import { deleteCookie, getCookie, setCookie } from 'hono/cookie' import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
import { config } from '../config.js' import { config } from '../config.js'
import { db, now, pruneSessions, type Media, type Token, type User } from '../db.js' import { db, now, pruneSessions, type Media, type Token, type User } from '../db.js'
@@ -25,13 +26,27 @@ type Vars = { user: User }
export const dashRoutes = new Hono<{ Variables: Vars }>() export const dashRoutes = new Hono<{ Variables: Vars }>()
/** Den JSON-Rumpf lesen — oder null, wenn keiner ankam.
*
* c.req.json() wirft bei kaputtem JSON, und Hono macht daraus einen nackten
* "Internal Server Error" ohne ein Wort dazu. Genau diese Sorte Antwort ist
* der Grund, warum wir hier neu bauen. */
async function jsonBody<T>(c: Context<any>): Promise<T | null> {
try {
return await c.req.json<T>()
} catch {
return null
}
}
const KEIN_JSON = { error: 'Rumpf ist kein gueltiges JSON' } as const
// ---------------------------------------------------------------- Anmeldung // ---------------------------------------------------------------- Anmeldung
dashRoutes.post('/auth/login', async (c) => { dashRoutes.post('/auth/login', async (c) => {
const { username, password } = await c.req.json<{ const body = await jsonBody<{ username?: string; password?: string }>(c)
username?: string if (!body) return c.json(KEIN_JSON, 400)
password?: string const { username, password } = body
}>()
if (!username || !password) { if (!username || !password) {
return c.json({ error: 'Benutzername und Passwort noetig' }, 400) return c.json({ error: 'Benutzername und Passwort noetig' }, 400)
@@ -81,10 +96,9 @@ dashRoutes.use('*', async (c, next) => {
}) })
dashRoutes.post('/auth/password', async (c) => { dashRoutes.post('/auth/password', async (c) => {
const { current, next: fresh } = await c.req.json<{ const body = await jsonBody<{ current?: string; next?: string }>(c)
current?: string if (!body) return c.json(KEIN_JSON, 400)
next?: string const { current, next: fresh } = body
}>()
const user = c.get('user') const user = c.get('user')
if (!current || !fresh) return c.json({ error: 'beide Passwoerter noetig' }, 400) if (!current || !fresh) return c.json({ error: 'beide Passwoerter noetig' }, 400)
@@ -145,7 +159,9 @@ dashRoutes.delete('/media/:id', async (c) => {
/** Mehrere auf einmal — bei 900 Fahrzeugbildern will niemand 900 Mal klicken. */ /** Mehrere auf einmal — bei 900 Fahrzeugbildern will niemand 900 Mal klicken. */
dashRoutes.post('/media/delete', async (c) => { dashRoutes.post('/media/delete', async (c) => {
const { ids } = await c.req.json<{ ids?: number[] }>() const body = await jsonBody<{ ids?: number[] }>(c)
if (!body) return c.json(KEIN_JSON, 400)
const { ids } = body
if (!Array.isArray(ids) || ids.length === 0) { if (!Array.isArray(ids) || ids.length === 0) {
return c.json({ error: 'keine Auswahl' }, 400) return c.json({ error: 'keine Auswahl' }, 400)
} }
@@ -197,11 +213,13 @@ dashRoutes.get('/tokens', (c) => {
}) })
dashRoutes.post('/tokens', async (c) => { dashRoutes.post('/tokens', async (c) => {
const { name, prefix, canDelete } = await c.req.json<{ const body = await jsonBody<{
name?: string name?: string
prefix?: string prefix?: string
canDelete?: boolean canDelete?: boolean
}>() }>(c)
if (!body) return c.json(KEIN_JSON, 400)
const { name, prefix, canDelete } = body
if (!name?.trim()) return c.json({ error: 'Name fehlt' }, 400) if (!name?.trim()) return c.json({ error: 'Name fehlt' }, 400)
+23 -8
View File
@@ -20,7 +20,7 @@
// dem NUI, ist also schon Base64 — der Umweg ist damit keiner. // dem NUI, ist also schon Base64 — der Umweg ist damit keiner.
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Context } from 'hono' import type { Context, Next } from 'hono'
import { config } from '../config.js' import { config } from '../config.js'
import { db, now, type Media, type Token } from '../db.js' import { db, now, type Media, type Token } from '../db.js'
import { tokenAllows, tokenFromHeader } from '../auth.js' import { tokenAllows, tokenFromHeader } from '../auth.js'
@@ -40,14 +40,23 @@ type Vars = { token: Token }
export const uploadRoutes = new Hono<{ Variables: Vars }>() export const uploadRoutes = new Hono<{ Variables: Vars }>()
uploadRoutes.use('*', async (c, next) => { // Die Token-Pruefung haengt an den drei eigenen Pfaden und NICHT an '*'.
//
// Mit '*' galt sie fuer alles unterhalb des Einhaengepunkts — auch fuer
// /api/dash/*, das daneben liegt. Das Dashboard bekam dann "Token fehlt oder
// ist unbekannt" auf die Anmeldung, obwohl es nie einen Token haben kann.
const requireToken = async (c: Context<{ Variables: Vars }>, next: Next) => {
const token = tokenFromHeader(c.req.header('authorization')) const token = tokenFromHeader(c.req.header('authorization'))
if (!token) { if (!token) {
return c.json({ error: 'Token fehlt oder ist unbekannt' }, 401) return c.json({ error: 'Token fehlt oder ist unbekannt' }, 401)
} }
c.set('token', token) c.set('token', token)
await next() await next()
}) }
uploadRoutes.use('/upload', requireToken)
uploadRoutes.use('/media/*', requireToken)
uploadRoutes.use('/exists/*', requireToken)
/** Den Rumpf einsammeln, in welcher der drei Formen er auch kommt. */ /** Den Rumpf einsammeln, in welcher der drei Formen er auch kommt. */
async function readBody( async function readBody(
@@ -179,8 +188,14 @@ uploadRoutes.post('/upload', async (c) => {
}) })
}) })
/** Loeschen ueber den Pfad — Skripte kennen den Pfad, nicht unsere ID. */ /** Loeschen ueber den Pfad — Skripte kennen den Pfad, nicht unsere ID.
uploadRoutes.delete('/media/*', async (c) => { *
* `:pfad{.+}` und nicht `*`: ein Sternchen wird von Hono nicht als Parameter
* erfasst, und `c.req.path` traegt den Einhaengepunkt mit — aus
* DELETE /api/media/vehicles/adder.png wurde damit der Pfad
* "api/media/vehicles/adder.png". Der Parameter kommt bereits dekodiert,
* ein zweites decodeURIComponent waere eine Dekodierung zu viel. */
uploadRoutes.delete('/media/:pfad{.+}', async (c) => {
const token = c.get('token') const token = c.get('token')
if (!token.can_delete) { if (!token.can_delete) {
return c.json({ error: 'dieser Token darf nicht loeschen' }, 403) return c.json({ error: 'dieser Token darf nicht loeschen' }, 403)
@@ -188,7 +203,7 @@ uploadRoutes.delete('/media/*', async (c) => {
let path: string let path: string
try { try {
path = checkPath(decodeURIComponent(c.req.path.replace(/^\/media\//, ''))) path = checkPath(c.req.param('pfad'))
} catch (err) { } catch (err) {
if (err instanceof PathError) return c.json({ error: err.message }, 400) if (err instanceof PathError) return c.json({ error: err.message }, 400)
throw err throw err
@@ -206,10 +221,10 @@ uploadRoutes.delete('/media/*', async (c) => {
/** Nachsehen, ob es etwas schon gibt — damit ein Lauf "nur fehlende" /** Nachsehen, ob es etwas schon gibt — damit ein Lauf "nur fehlende"
* beantworten kann, ohne 900 Bilder hochzuladen. */ * beantworten kann, ohne 900 Bilder hochzuladen. */
uploadRoutes.get('/exists/*', (c) => { uploadRoutes.get('/exists/:pfad{.+}', (c) => {
let path: string let path: string
try { try {
path = checkPath(decodeURIComponent(c.req.path.replace(/^\/exists\//, ''))) path = checkPath(c.req.param('pfad'))
} catch { } catch {
return c.json({ exists: false }, 200) return c.json({ exists: false }, 200)
} }