minor bugfixes

This commit is contained in:
Ludwig Lehnert
2026-03-28 09:10:40 +01:00
parent 48bfe69d09
commit ee72edecb1
7 changed files with 94 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
SERVICE_FQDN=files.example.com SERVICE_FQDN=files.example.com
LETSENCRYPT_EMAIL=user@example.com LETSENCRYPT_EMAIL=user@example.com
PUBLIC_BASE_URL=
DATA_DIR=/storagebox DATA_DIR=/storagebox
UPLOAD_TTL_SECONDS=604800 UPLOAD_TTL_SECONDS=604800
UPLOAD_MAX_BYTES=0 UPLOAD_MAX_BYTES=0

View File

@@ -24,6 +24,7 @@ File server infrastructure hosted on [files.lehnert.cloud](https://files.lehnert
Danach: Danach:
1. `.env` anpassen (`SERVICE_FQDN`, `LETSENCRYPT_EMAIL`, `DATA_DIR`, `UPLOAD_TTL_SECONDS`, `MANAGEMENT_ADMIN_HASH`, optional `UPLOAD_MAX_BYTES` und `COOKIE_SECURE`) 1. `.env` anpassen (`SERVICE_FQDN`, `LETSENCRYPT_EMAIL`, `DATA_DIR`, `UPLOAD_TTL_SECONDS`, `MANAGEMENT_ADMIN_HASH`, optional `UPLOAD_MAX_BYTES` und `COOKIE_SECURE`)
2. Für Upload-Anfragen mit E-Mail-Benachrichtigung SMTP setzen (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_MAIL`, `SMTP_NAME`; Absender: `SMTP_NAME <SMTP_MAIL>`) 2. Optional `PUBLIC_BASE_URL` setzen, falls absolute Links in E-Mails einen festen Host verwenden sollen
3. Stack starten: `docker compose up --build` 3. Für Upload-Anfragen mit E-Mail-Benachrichtigung SMTP setzen (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_MAIL`, `SMTP_NAME`; Absender: `SMTP_NAME <SMTP_MAIL>`)
4. Als Admin anmelden und Benutzer über die UI anlegen 4. Stack starten: `docker compose up --build`
5. Als Admin anmelden und Benutzer über die UI anlegen

View File

@@ -65,6 +65,8 @@ services:
environment: environment:
- DATA_DIR=/data - DATA_DIR=/data
- DB_PATH=/app/data/uploads.sqlite - DB_PATH=/app/data/uploads.sqlite
- SERVICE_FQDN=${SERVICE_FQDN}
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL}
- UPLOAD_TTL_SECONDS=${UPLOAD_TTL_SECONDS} - UPLOAD_TTL_SECONDS=${UPLOAD_TTL_SECONDS}
- UPLOAD_MAX_BYTES=${UPLOAD_MAX_BYTES} - UPLOAD_MAX_BYTES=${UPLOAD_MAX_BYTES}
- MANAGEMENT_ADMIN_HASH=${MANAGEMENT_ADMIN_HASH} - MANAGEMENT_ADMIN_HASH=${MANAGEMENT_ADMIN_HASH}

View File

@@ -21,7 +21,7 @@ fi
echo "Initialization complete." echo "Initialization complete."
echo "Next steps:" echo "Next steps:"
echo "1) Edit .env and set SERVICE_FQDN, LETSENCRYPT_EMAIL, DATA_DIR, UPLOAD_TTL_SECONDS, optional UPLOAD_MAX_BYTES" echo "1) Edit .env and set SERVICE_FQDN, LETSENCRYPT_EMAIL, DATA_DIR, UPLOAD_TTL_SECONDS, optional UPLOAD_MAX_BYTES/PUBLIC_BASE_URL"
echo "2) Set MANAGEMENT_ADMIN_HASH in .env for admin login" echo "2) Set MANAGEMENT_ADMIN_HASH in .env for admin login"
echo "3) Optional for upload-request notifications: set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_MAIL, SMTP_NAME" echo "3) Optional for upload-request notifications: set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_MAIL, SMTP_NAME"
echo "4) Start with docker compose up --build" echo "4) Start with docker compose up --build"

View File

@@ -9,6 +9,7 @@ import { NextResponse } from 'next/server';
import { import {
maxRetentionSeconds, maxRetentionSeconds,
maxUploadBytes, maxUploadBytes,
publicBaseUrl,
shareDir, shareDir,
uploadTtlSeconds, uploadTtlSeconds,
} from '@/src/lib/config.js'; } from '@/src/lib/config.js';
@@ -41,8 +42,49 @@ function requestPageHref(requestId, params = {}) {
return serialized ? `/_request/${encodedId}?${serialized}` : `/_request/${encodedId}`; return serialized ? `/_request/${encodedId}?${serialized}` : `/_request/${encodedId}`;
} }
function redirectToRequest(request, requestId, params = {}) { function redirectToRequest(requestId, params = {}) {
return NextResponse.redirect(new URL(requestPageHref(requestId, params), request.url), { status: 303 }); return new NextResponse(null, {
status: 303,
headers: {
location: requestPageHref(requestId, params),
},
});
}
function firstForwardedPart(value) {
if (!value) {
return '';
}
return value.split(',')[0].trim();
}
function requestOrigin(request) {
const forwardedHost = firstForwardedPart(request.headers.get('x-forwarded-host'));
const host = forwardedHost || request.headers.get('host') || '';
if (!host) {
return '';
}
const forwardedProto = firstForwardedPart(request.headers.get('x-forwarded-proto'));
const proto = forwardedProto || (host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https');
return `${proto}://${host}`;
}
function resolvePublicOrigin(request) {
if (publicBaseUrl) {
return publicBaseUrl;
}
const fromHeaders = requestOrigin(request);
if (fromHeaders) {
return fromHeaders;
}
try {
return new URL(request.url).origin;
} catch {
return '';
}
} }
function toBase32(buffer) { function toBase32(buffer) {
@@ -119,7 +161,7 @@ export async function POST(request, { params }) {
try { try {
formData = await request.formData(); formData = await request.formData();
} catch { } catch {
return redirectToRequest(request, requestId, { error: 'Ungültige Formulardaten.' }); return redirectToRequest(requestId, { error: 'Ungültige Formulardaten.' });
} }
const uploadRequest = await get( const uploadRequest = await get(
@@ -130,25 +172,25 @@ export async function POST(request, { params }) {
); );
if (!uploadRequest) { if (!uploadRequest) {
return redirectToRequest(request, requestId, { error: 'Anfrage nicht gefunden.' }); return redirectToRequest(requestId, { error: 'Anfrage nicht gefunden.' });
} }
if (Number(uploadRequest.completed_at || 0) > 0) { if (Number(uploadRequest.completed_at || 0) > 0) {
return redirectToRequest(request, requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' }); return redirectToRequest(requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' });
} }
const now = Date.now(); const now = Date.now();
if (Number(uploadRequest.expires_at || 0) <= now) { if (Number(uploadRequest.expires_at || 0) <= now) {
return redirectToRequest(request, requestId, { error: 'Diese Anfrage ist bereits abgelaufen.' }); return redirectToRequest(requestId, { error: 'Diese Anfrage ist bereits abgelaufen.' });
} }
const uploadedFile = uploadedFileFromForm(formData, 'file'); const uploadedFile = uploadedFileFromForm(formData, 'file');
if (!uploadedFile || Number(uploadedFile.size || 0) <= 0) { if (!uploadedFile || Number(uploadedFile.size || 0) <= 0) {
return redirectToRequest(request, requestId, { error: 'Keine Datei hochgeladen.' }); return redirectToRequest(requestId, { error: 'Keine Datei hochgeladen.' });
} }
if (maxUploadBytes > 0 && Number(uploadedFile.size || 0) > maxUploadBytes) { if (maxUploadBytes > 0 && Number(uploadedFile.size || 0) > maxUploadBytes) {
return redirectToRequest(request, requestId, { return redirectToRequest(requestId, {
error: `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`, error: `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`,
}); });
} }
@@ -171,7 +213,7 @@ export async function POST(request, { params }) {
} }
if (!storedName || !storedPath) { if (!storedName || !storedPath) {
return redirectToRequest(request, requestId, { error: 'Upload-ID konnte nicht erzeugt werden.' }); return redirectToRequest(requestId, { error: 'Upload-ID konnte nicht erzeugt werden.' });
} }
const uploadExpiry = Math.min(now + uploadTtlSeconds * 1000, now + maxRetentionSeconds * 1000); const uploadExpiry = Math.min(now + uploadTtlSeconds * 1000, now + maxRetentionSeconds * 1000);
@@ -187,7 +229,7 @@ export async function POST(request, { params }) {
); );
uploadId = insertResult.lastID; uploadId = insertResult.lastID;
} catch { } catch {
return redirectToRequest(request, requestId, { error: 'Upload fehlgeschlagen.' }); return redirectToRequest(requestId, { error: 'Upload fehlgeschlagen.' });
} }
const updateResult = await run( const updateResult = await run(
@@ -206,7 +248,7 @@ export async function POST(request, { params }) {
if (!updateResult || updateResult.changes < 1) { if (!updateResult || updateResult.changes < 1) {
await run('DELETE FROM uploads WHERE id = ?', [uploadId]).catch(() => undefined); await run('DELETE FROM uploads WHERE id = ?', [uploadId]).catch(() => undefined);
await fs.promises.rm(storedPath, { force: true }).catch(() => undefined); await fs.promises.rm(storedPath, { force: true }).catch(() => undefined);
return redirectToRequest(request, requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' }); return redirectToRequest(requestId, { success: 'Diese Anfrage wurde bereits erfüllt.' });
} }
await logEvent( await logEvent(
@@ -216,7 +258,7 @@ export async function POST(request, { params }) {
await getRequestMeta() await getRequestMeta()
); );
const baseUrl = new URL(request.url).origin; const baseUrl = resolvePublicOrigin(request);
const downloadUrl = `${baseUrl}/_share/${encodeURIComponent(storedName)}`; const downloadUrl = `${baseUrl}/_share/${encodeURIComponent(storedName)}`;
const mailResult = await sendUploadRequestCompletedMail({ const mailResult = await sendUploadRequestCompletedMail({
@@ -230,7 +272,7 @@ export async function POST(request, { params }) {
if (mailResult.ok) { if (mailResult.ok) {
await run('UPDATE upload_requests SET notification_sent_at = ? WHERE id = ?', [Date.now(), requestId]); await run('UPDATE upload_requests SET notification_sent_at = ? WHERE id = ?', [Date.now(), requestId]);
return redirectToRequest(request, requestId, { success: 'Datei erfolgreich hochgeladen. Vielen Dank!' }); return redirectToRequest(requestId, { success: 'Datei erfolgreich hochgeladen. Vielen Dank!' });
} }
await logEvent( await logEvent(
@@ -240,7 +282,7 @@ export async function POST(request, { params }) {
await getRequestMeta() await getRequestMeta()
); );
return redirectToRequest(request, requestId, { return redirectToRequest(requestId, {
success: 'Datei hochgeladen. Hinweis: E-Mail-Benachrichtigung konnte nicht gesendet werden.', success: 'Datei hochgeladen. Hinweis: E-Mail-Benachrichtigung konnte nicht gesendet werden.',
}); });
} }

View File

@@ -107,8 +107,12 @@ function expectsHtml(request) {
function errorResponse(request, message, status = 400) { function errorResponse(request, message, status = 400) {
if (expectsHtml(request)) { if (expectsHtml(request)) {
const target = new URL(dashboardHref({ error: message }), request.url); return new NextResponse(null, {
return NextResponse.redirect(target, { status: 303 }); status: 303,
headers: {
location: dashboardHref({ error: message }),
},
});
} }
return NextResponse.json({ ok: false, error: message }, { status }); return NextResponse.json({ ok: false, error: message }, { status });
@@ -117,8 +121,12 @@ function errorResponse(request, message, status = 400) {
function successResponse(request, message) { function successResponse(request, message) {
const redirectPath = dashboardHref({ success: message }); const redirectPath = dashboardHref({ success: message });
if (expectsHtml(request)) { if (expectsHtml(request)) {
const target = new URL(redirectPath, request.url); return new NextResponse(null, {
return NextResponse.redirect(target, { status: 303 }); status: 303,
headers: {
location: redirectPath,
},
});
} }
return NextResponse.json({ ok: true, redirect: redirectPath }); return NextResponse.json({ ok: true, redirect: redirectPath });

View File

@@ -5,15 +5,33 @@ function parseInteger(value, fallback) {
return Number.isFinite(parsed) ? parsed : fallback; return Number.isFinite(parsed) ? parsed : fallback;
} }
function normalizeBaseUrl(value) {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
const withProtocol = raw.includes('://') ? raw : `https://${raw}`;
try {
const parsed = new URL(withProtocol);
return `${parsed.protocol}//${parsed.host}`;
} catch {
return '';
}
}
export const managementBasePath = '/manage'; export const managementBasePath = '/manage';
export const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data'); export const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data');
export const dbPath = process.env.DB_PATH || path.join(dataDir, 'uploads.sqlite'); export const dbPath = process.env.DB_PATH || path.join(dataDir, 'uploads.sqlite');
export const shareDir = path.join(dataDir, '_share'); export const shareDir = path.join(dataDir, '_share');
export const serviceFqdn = String(process.env.SERVICE_FQDN || '').trim();
export const adminHash = process.env.MANAGEMENT_ADMIN_HASH || ''; export const adminHash = process.env.MANAGEMENT_ADMIN_HASH || '';
export const uploadTtlSeconds = parseInteger(process.env.UPLOAD_TTL_SECONDS || '604800', 604800); export const uploadTtlSeconds = parseInteger(process.env.UPLOAD_TTL_SECONDS || '604800', 604800);
export const maxRetentionSeconds = 90 * 24 * 60 * 60; export const maxRetentionSeconds = 90 * 24 * 60 * 60;
export const maxUploadBytes = parseInteger(process.env.UPLOAD_MAX_BYTES || '0', 0); export const maxUploadBytes = parseInteger(process.env.UPLOAD_MAX_BYTES || '0', 0);
export const cookieSecure = process.env.COOKIE_SECURE === 'true'; export const cookieSecure = process.env.COOKIE_SECURE === 'true';
export const publicBaseUrl = normalizeBaseUrl(process.env.PUBLIC_BASE_URL || serviceFqdn);
export const smtpHost = String(process.env.SMTP_HOST || '').trim(); export const smtpHost = String(process.env.SMTP_HOST || '').trim();
export const smtpPort = parseInteger(process.env.SMTP_PORT || '587', 587); export const smtpPort = parseInteger(process.env.SMTP_PORT || '587', 587);
export const smtpUser = String(process.env.SMTP_USER || '').trim(); export const smtpUser = String(process.env.SMTP_USER || '').trim();