minor bugfixes
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user