minor bugfixes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
SERVICE_FQDN=files.example.com
|
||||
LETSENCRYPT_EMAIL=user@example.com
|
||||
PUBLIC_BASE_URL=
|
||||
DATA_DIR=/storagebox
|
||||
UPLOAD_TTL_SECONDS=604800
|
||||
UPLOAD_MAX_BYTES=0
|
||||
|
||||
@@ -24,6 +24,7 @@ File server infrastructure hosted on [files.lehnert.cloud](https://files.lehnert
|
||||
Danach:
|
||||
|
||||
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>`)
|
||||
3. Stack starten: `docker compose up --build`
|
||||
4. Als Admin anmelden und Benutzer über die UI anlegen
|
||||
2. Optional `PUBLIC_BASE_URL` setzen, falls absolute Links in E-Mails einen festen Host verwenden sollen
|
||||
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. Stack starten: `docker compose up --build`
|
||||
5. Als Admin anmelden und Benutzer über die UI anlegen
|
||||
|
||||
@@ -65,6 +65,8 @@ services:
|
||||
environment:
|
||||
- DATA_DIR=/data
|
||||
- DB_PATH=/app/data/uploads.sqlite
|
||||
- SERVICE_FQDN=${SERVICE_FQDN}
|
||||
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL}
|
||||
- UPLOAD_TTL_SECONDS=${UPLOAD_TTL_SECONDS}
|
||||
- UPLOAD_MAX_BYTES=${UPLOAD_MAX_BYTES}
|
||||
- MANAGEMENT_ADMIN_HASH=${MANAGEMENT_ADMIN_HASH}
|
||||
|
||||
@@ -21,7 +21,7 @@ fi
|
||||
|
||||
echo "Initialization complete."
|
||||
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 "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"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NextResponse } from 'next/server';
|
||||
import {
|
||||
maxRetentionSeconds,
|
||||
maxUploadBytes,
|
||||
publicBaseUrl,
|
||||
shareDir,
|
||||
uploadTtlSeconds,
|
||||
} from '@/src/lib/config.js';
|
||||
@@ -41,8 +42,49 @@ function requestPageHref(requestId, params = {}) {
|
||||
return serialized ? `/_request/${encodedId}?${serialized}` : `/_request/${encodedId}`;
|
||||
}
|
||||
|
||||
function redirectToRequest(request, requestId, params = {}) {
|
||||
return NextResponse.redirect(new URL(requestPageHref(requestId, params), request.url), { status: 303 });
|
||||
function redirectToRequest(requestId, params = {}) {
|
||||
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) {
|
||||
@@ -119,7 +161,7 @@ export async function POST(request, { params }) {
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch {
|
||||
return redirectToRequest(request, requestId, { error: 'Ungültige Formulardaten.' });
|
||||
return redirectToRequest(requestId, { error: 'Ungültige Formulardaten.' });
|
||||
}
|
||||
|
||||
const uploadRequest = await get(
|
||||
@@ -130,25 +172,25 @@ export async function POST(request, { params }) {
|
||||
);
|
||||
|
||||
if (!uploadRequest) {
|
||||
return redirectToRequest(request, requestId, { error: 'Anfrage nicht gefunden.' });
|
||||
return redirectToRequest(requestId, { error: 'Anfrage nicht gefunden.' });
|
||||
}
|
||||
|
||||
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();
|
||||
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');
|
||||
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) {
|
||||
return redirectToRequest(request, requestId, {
|
||||
return redirectToRequest(requestId, {
|
||||
error: `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`,
|
||||
});
|
||||
}
|
||||
@@ -171,7 +213,7 @@ export async function POST(request, { params }) {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -187,7 +229,7 @@ export async function POST(request, { params }) {
|
||||
);
|
||||
uploadId = insertResult.lastID;
|
||||
} catch {
|
||||
return redirectToRequest(request, requestId, { error: 'Upload fehlgeschlagen.' });
|
||||
return redirectToRequest(requestId, { error: 'Upload fehlgeschlagen.' });
|
||||
}
|
||||
|
||||
const updateResult = await run(
|
||||
@@ -206,7 +248,7 @@ export async function POST(request, { params }) {
|
||||
if (!updateResult || updateResult.changes < 1) {
|
||||
await run('DELETE FROM uploads WHERE id = ?', [uploadId]).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(
|
||||
@@ -216,7 +258,7 @@ export async function POST(request, { params }) {
|
||||
await getRequestMeta()
|
||||
);
|
||||
|
||||
const baseUrl = new URL(request.url).origin;
|
||||
const baseUrl = resolvePublicOrigin(request);
|
||||
const downloadUrl = `${baseUrl}/_share/${encodeURIComponent(storedName)}`;
|
||||
|
||||
const mailResult = await sendUploadRequestCompletedMail({
|
||||
@@ -230,7 +272,7 @@ export async function POST(request, { params }) {
|
||||
|
||||
if (mailResult.ok) {
|
||||
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(
|
||||
@@ -240,7 +282,7 @@ export async function POST(request, { params }) {
|
||||
await getRequestMeta()
|
||||
);
|
||||
|
||||
return redirectToRequest(request, requestId, {
|
||||
return redirectToRequest(requestId, {
|
||||
success: 'Datei hochgeladen. Hinweis: E-Mail-Benachrichtigung konnte nicht gesendet werden.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,8 +107,12 @@ function expectsHtml(request) {
|
||||
|
||||
function errorResponse(request, message, status = 400) {
|
||||
if (expectsHtml(request)) {
|
||||
const target = new URL(dashboardHref({ error: message }), request.url);
|
||||
return NextResponse.redirect(target, { status: 303 });
|
||||
return new NextResponse(null, {
|
||||
status: 303,
|
||||
headers: {
|
||||
location: dashboardHref({ error: message }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: false, error: message }, { status });
|
||||
@@ -117,8 +121,12 @@ function errorResponse(request, message, status = 400) {
|
||||
function successResponse(request, message) {
|
||||
const redirectPath = dashboardHref({ success: message });
|
||||
if (expectsHtml(request)) {
|
||||
const target = new URL(redirectPath, request.url);
|
||||
return NextResponse.redirect(target, { status: 303 });
|
||||
return new NextResponse(null, {
|
||||
status: 303,
|
||||
headers: {
|
||||
location: redirectPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, redirect: redirectPath });
|
||||
|
||||
@@ -5,15 +5,33 @@ function parseInteger(value, 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 dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
||||
export const dbPath = process.env.DB_PATH || path.join(dataDir, 'uploads.sqlite');
|
||||
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 uploadTtlSeconds = parseInteger(process.env.UPLOAD_TTL_SECONDS || '604800', 604800);
|
||||
export const maxRetentionSeconds = 90 * 24 * 60 * 60;
|
||||
export const maxUploadBytes = parseInteger(process.env.UPLOAD_MAX_BYTES || '0', 0);
|
||||
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 smtpPort = parseInteger(process.env.SMTP_PORT || '587', 587);
|
||||
export const smtpUser = String(process.env.SMTP_USER || '').trim();
|
||||
|
||||
Reference in New Issue
Block a user