feat: improve upload admin workflow

Add users.is_admin SQLite migration for promoted admins.

Support curl PUT request uploads, save uploads into admin file-manager paths, default 25 GiB uploads, tabbed UI, and local dev preview.
This commit is contained in:
Ludwig Lehnert
2026-06-18 17:09:07 +00:00
parent 61f10fd994
commit a2b6054440
19 changed files with 1148 additions and 396 deletions

View File

@@ -37,7 +37,8 @@ import {
} from './security.js';
function buildPathWithQuery(pathname, params = {}) {
const query = new URLSearchParams();
const [pathOnly, existingQuery = ''] = String(pathname).split('?');
const query = new URLSearchParams(existingQuery);
for (const [key, value] of Object.entries(params)) {
if (value) {
@@ -46,7 +47,7 @@ function buildPathWithQuery(pathname, params = {}) {
}
const serialized = query.toString();
return serialized ? `${pathname}?${serialized}` : pathname;
return serialized ? `${pathOnly}?${serialized}` : pathOnly;
}
function redirectWithError(pathname, message) {
@@ -57,6 +58,10 @@ function redirectWithSuccess(pathname, message) {
redirect(buildPathWithQuery(pathname, { success: message }));
}
function adminFilesTabHref(relativePath, tab) {
return buildPathWithQuery(adminFilesHref(relativePath), { tab });
}
function getUploadId(formData, fieldName = 'uploadId') {
const parsed = Number.parseInt(String(formData.get(fieldName) || ''), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
@@ -156,6 +161,33 @@ async function copyPath(sourcePath, targetPath) {
});
}
function savedUploadFileName(originalName, storedName) {
const fallback = safeBaseName(storedName, 'upload');
const candidate = safeBaseName(originalName, fallback).replace(/[\\/]/g, '_');
return isValidNodeName(candidate) ? candidate : fallback;
}
async function resolveSavedUploadTarget(targetRelativePath, fileName) {
const targetPath = resolveAdminPath(targetRelativePath);
if (!targetPath) {
return null;
}
try {
const targetStat = await fs.promises.stat(targetPath);
if (targetStat.isDirectory()) {
return path.join(targetPath, fileName);
}
} catch {
}
if (!isValidNodeName(path.basename(targetPath))) {
return null;
}
return targetPath;
}
export async function userLoginAction(formData) {
await runCleanupIfNeeded();
@@ -175,13 +207,13 @@ export async function userLoginAction(formData) {
const username = String(formData.get('username') || '').trim();
const password = String(formData.get('password') || '');
const row = await get('SELECT password_hash FROM users WHERE username = ?', [username]);
const row = await get('SELECT password_hash, is_admin FROM users WHERE username = ?', [username]);
if (!row || !bcrypt.compareSync(password, row.password_hash)) {
redirectWithError(`${managementBasePath}/login`, 'Anmeldung fehlgeschlagen.');
}
await setAuthCookie({ sub: username });
await setAuthCookie({ sub: username, admin: Boolean(row.is_admin), auth: 'user' });
await clearLoginRateLimit('user');
await logEvent('login', username, { ok: true }, await getRequestMeta());
redirect(`${managementBasePath}/dashboard`);
@@ -191,7 +223,7 @@ export async function userLogoutAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/dashboard?tab=upload`, 'CSRF-Prüfung fehlgeschlagen.');
}
await clearAuthCookie();
@@ -222,7 +254,7 @@ export async function adminLoginAction(formData) {
redirectWithError(`${managementBasePath}/admin`, 'Anmeldung fehlgeschlagen.');
}
await setAuthCookie({ sub: 'admin', admin: true });
await setAuthCookie({ sub: 'admin', admin: true, auth: 'admin' });
await clearLoginRateLimit('admin');
await logEvent('admin_login', 'admin', { ok: true }, await getRequestMeta());
redirect(`${managementBasePath}/admin/dashboard`);
@@ -232,7 +264,7 @@ export async function adminLogoutAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'CSRF-Prüfung fehlgeschlagen.');
}
await clearAuthCookie();
@@ -245,18 +277,18 @@ export async function uploadFileAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'CSRF-Prüfung fehlgeschlagen.');
}
const user = await requireAuthenticatedUser();
const uploadedFile = uploadedFileFromForm(formData, 'file');
if (!uploadedFile || Number(uploadedFile.size || 0) <= 0) {
redirectWithError(`${managementBasePath}/dashboard`, 'Keine Datei hochgeladen.');
redirectWithError(`${managementBasePath}/dashboard?tab=upload`, 'Keine Datei hochgeladen.');
}
if (maxUploadBytes > 0 && Number(uploadedFile.size || 0) > maxUploadBytes) {
redirectWithError(
`${managementBasePath}/dashboard`,
`${managementBasePath}/dashboard?tab=upload`,
`Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`
);
}
@@ -285,7 +317,7 @@ export async function uploadFileAction(formData) {
]
);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'Upload fehlgeschlagen.');
redirectWithError(`${managementBasePath}/dashboard?tab=upload`, 'Upload fehlgeschlagen.');
}
await logEvent(
@@ -295,7 +327,7 @@ export async function uploadFileAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Upload abgeschlossen.');
redirectWithSuccess(`${managementBasePath}/dashboard?tab=upload`, 'Upload abgeschlossen.');
}
export async function deleteOwnUploadAction(formData) {
@@ -308,7 +340,7 @@ export async function deleteOwnUploadAction(formData) {
const user = await requireAuthenticatedUser();
const uploadId = getUploadId(formData);
if (!uploadId) {
redirectWithError(`${managementBasePath}/dashboard`, 'Ungültige Upload-ID.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'Ungültige Upload-ID.');
}
const uploadEntry = await get(
@@ -317,7 +349,7 @@ export async function deleteOwnUploadAction(formData) {
);
if (!uploadEntry) {
redirectWithError(`${managementBasePath}/dashboard`, 'Upload nicht gefunden.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'Upload nicht gefunden.');
}
try {
@@ -327,20 +359,20 @@ export async function deleteOwnUploadAction(formData) {
await run('DELETE FROM uploads WHERE id = ?', [uploadEntry.id]);
await logEvent('delete', user.username, { id: uploadEntry.id }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Upload gelöscht.');
redirectWithSuccess(`${managementBasePath}/dashboard?tab=files`, 'Upload gelöscht.');
}
export async function extendOwnUploadAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'CSRF-Prüfung fehlgeschlagen.');
}
const user = await requireAuthenticatedUser();
const uploadId = getUploadId(formData);
if (!uploadId) {
redirectWithError(`${managementBasePath}/dashboard`, 'Ungültige Upload-ID.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'Ungültige Upload-ID.');
}
const uploadEntry = await get(
@@ -349,7 +381,7 @@ export async function extendOwnUploadAction(formData) {
);
if (!uploadEntry) {
redirectWithError(`${managementBasePath}/dashboard`, 'Upload nicht gefunden.');
redirectWithError(`${managementBasePath}/dashboard?tab=files`, 'Upload nicht gefunden.');
}
const extensionSeconds = parseHours(formData.get('extendHours'), uploadTtlSeconds);
@@ -366,7 +398,7 @@ export async function extendOwnUploadAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Aufbewahrung aktualisiert.');
redirectWithSuccess(`${managementBasePath}/dashboard?tab=files`, 'Aufbewahrung aktualisiert.');
}
export async function createUploadRequestAction(formData) {
@@ -375,7 +407,7 @@ export async function createUploadRequestAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/dashboard?tab=requests`, 'CSRF-Prüfung fehlgeschlagen.');
}
const user = await requireAuthenticatedUser();
@@ -395,7 +427,7 @@ export async function createUploadRequestAction(formData) {
}
if (!requestId) {
redirectWithError(`${managementBasePath}/dashboard`, 'Anfrage-ID konnte nicht erzeugt werden.');
redirectWithError(`${managementBasePath}/dashboard?tab=requests`, 'Anfrage-ID konnte nicht erzeugt werden.');
}
try {
@@ -404,7 +436,7 @@ export async function createUploadRequestAction(formData) {
[requestId, user.username, note || null, now, expiresAt]
);
} catch {
redirectWithError(`${managementBasePath}/dashboard`, 'Anfrage konnte nicht gespeichert werden.');
redirectWithError(`${managementBasePath}/dashboard?tab=requests`, 'Anfrage konnte nicht gespeichert werden.');
}
await logEvent(
@@ -414,44 +446,77 @@ export async function createUploadRequestAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Upload-Anfrage erstellt.');
redirectWithSuccess(`${managementBasePath}/dashboard?tab=requests`, 'Upload-Anfrage erstellt.');
}
export async function adminCreateUserAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/users`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/admin/users?tab=create`, 'CSRF-Prüfung fehlgeschlagen.');
}
await requireAdminUser();
const username = String(formData.get('username') || '').trim();
const password = String(formData.get('password') || '');
const isAdmin = String(formData.get('isAdmin') || '') === '1';
if (!username || username.length > 200 || !password) {
redirectWithError(`${managementBasePath}/admin/users`, 'Ungültige Eingabe.');
redirectWithError(`${managementBasePath}/admin/users?tab=create`, 'Ungültige Eingabe.');
}
const passwordHash = bcrypt.hashSync(password, 12);
try {
await run('INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)', [
await run('INSERT INTO users (username, password_hash, created_at, is_admin) VALUES (?, ?, ?, ?)', [
username,
passwordHash,
Date.now(),
isAdmin ? 1 : 0,
]);
} catch {
redirectWithError(`${managementBasePath}/admin/users`, 'Benutzername existiert bereits.');
redirectWithError(`${managementBasePath}/admin/users?tab=create`, 'Benutzername existiert bereits.');
}
await logEvent('admin_user_create', 'admin', { username }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/admin/users`, 'Benutzer erstellt.');
await logEvent('admin_user_create', 'admin', { username, is_admin: isAdmin }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/admin/users?tab=users`, 'Benutzer erstellt.');
}
export async function adminSetUserAdminAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'CSRF-Prüfung fehlgeschlagen.');
}
const actor = await requireAdminUser();
const username = String(formData.get('username') || '').trim();
const isAdmin = String(formData.get('isAdmin') || '') === '1';
if (!username) {
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Ungültige Eingabe.');
}
if (actor.username === username && !isAdmin) {
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Eigene Adminrechte können nicht entzogen werden.');
}
const result = await run('UPDATE users SET is_admin = ? WHERE username = ?', [isAdmin ? 1 : 0, username]);
if (!result || result.changes < 1) {
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Benutzer nicht gefunden.');
}
await logEvent('admin_user_role', 'admin', { username, is_admin: isAdmin }, await getRequestMeta());
redirectWithSuccess(
`${managementBasePath}/admin/users?tab=users`,
isAdmin ? 'Adminrechte vergeben.' : 'Adminrechte entzogen.'
);
}
export async function adminResetUserAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/users`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'CSRF-Prüfung fehlgeschlagen.');
}
await requireAdminUser();
@@ -459,32 +524,36 @@ export async function adminResetUserAction(formData) {
const username = String(formData.get('username') || '').trim();
const password = String(formData.get('password') || '');
if (!username || !password) {
redirectWithError(`${managementBasePath}/admin/users`, 'Ungültige Eingabe.');
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Ungültige Eingabe.');
}
const passwordHash = bcrypt.hashSync(password, 12);
await run('UPDATE users SET password_hash = ? WHERE username = ?', [passwordHash, username]);
await logEvent('admin_user_reset', 'admin', { username }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/admin/users`, 'Passwort aktualisiert.');
redirectWithSuccess(`${managementBasePath}/admin/users?tab=users`, 'Passwort aktualisiert.');
}
export async function adminDeleteUserAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/users`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'CSRF-Prüfung fehlgeschlagen.');
}
await requireAdminUser();
const actor = await requireAdminUser();
const username = String(formData.get('username') || '').trim();
if (!username) {
redirectWithError(`${managementBasePath}/admin/users`, 'Ungültige Eingabe.');
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Ungültige Eingabe.');
}
if (actor.username === username) {
redirectWithError(`${managementBasePath}/admin/users?tab=users`, 'Eigener Benutzer kann nicht gelöscht werden.');
}
await run('DELETE FROM users WHERE username = ?', [username]);
await logEvent('admin_user_delete', 'admin', { username }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/admin/users`, 'Benutzer gelöscht.');
redirectWithSuccess(`${managementBasePath}/admin/users?tab=users`, 'Benutzer gelöscht.');
}
export async function adminDeleteUploadAction(formData) {
@@ -498,12 +567,12 @@ export async function adminDeleteUploadAction(formData) {
const uploadId = getUploadId(formData);
if (!uploadId) {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'Ungültige Upload-ID.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Ungültige Upload-ID.');
}
const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ?', [uploadId]);
if (!uploadEntry) {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'Upload nicht gefunden.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Upload nicht gefunden.');
}
try {
@@ -513,26 +582,26 @@ export async function adminDeleteUploadAction(formData) {
await run('DELETE FROM uploads WHERE id = ?', [uploadEntry.id]);
await logEvent('delete', 'admin', { id: uploadEntry.id }, await getRequestMeta());
redirectWithSuccess(`${managementBasePath}/admin/dashboard`, 'Upload gelöscht.');
redirectWithSuccess(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Upload gelöscht.');
}
export async function adminExtendUploadAction(formData) {
try {
await verifyCsrf(formData);
} catch {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'CSRF-Prüfung fehlgeschlagen.');
}
await requireAdminUser();
const uploadId = getUploadId(formData);
if (!uploadId) {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'Ungültige Upload-ID.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Ungültige Upload-ID.');
}
const uploadEntry = await get('SELECT id, expires_at FROM uploads WHERE id = ?', [uploadId]);
if (!uploadEntry) {
redirectWithError(`${managementBasePath}/admin/dashboard`, 'Upload nicht gefunden.');
redirectWithError(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Upload nicht gefunden.');
}
const extensionSeconds = parseHours(formData.get('extendHours'), uploadTtlSeconds);
@@ -549,7 +618,61 @@ export async function adminExtendUploadAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(`${managementBasePath}/admin/dashboard`, 'Aufbewahrung aktualisiert.');
redirectWithSuccess(`${managementBasePath}/admin/dashboard?tab=uploads`, 'Aufbewahrung aktualisiert.');
}
export async function adminSaveUploadToPathAction(formData) {
const requestedTab = String(formData.get('redirectTab') || 'uploads');
const redirectTab = ['uploads', 'requests'].includes(requestedTab) ? requestedTab : 'uploads';
const dashboardPath = `${managementBasePath}/admin/dashboard?tab=${redirectTab}`;
try {
await verifyCsrf(formData);
} catch {
redirectWithError(dashboardPath, 'CSRF-Prüfung fehlgeschlagen.');
}
await requireAdminUser();
const uploadId = getUploadId(formData);
const rawTargetPath = String(formData.get('targetPath') || '').trim();
const targetRelativePath = sanitizeRelativePath(rawTargetPath);
if (!uploadId || !rawTargetPath || !targetRelativePath) {
redirectWithError(dashboardPath, 'Upload und Zielpfad sind erforderlich.');
}
const uploadEntry = await get(
'SELECT id, original_name, stored_name, stored_path FROM uploads WHERE id = ?',
[uploadId]
);
if (!uploadEntry) {
redirectWithError(dashboardPath, 'Upload nicht gefunden.');
}
const fileName = savedUploadFileName(uploadEntry.original_name, uploadEntry.stored_name);
const targetPath = await resolveSavedUploadTarget(targetRelativePath, fileName);
if (!targetPath) {
redirectWithError(dashboardPath, 'Ungültiger Zielpfad.');
}
try {
await fs.promises.cp(uploadEntry.stored_path, targetPath, { force: false, errorOnExist: true });
} catch {
redirectWithError(
dashboardPath,
'Upload konnte nicht gespeichert werden. Ziel existiert evtl. bereits.'
);
}
const relativeTarget = sanitizeRelativePath(path.relative(resolveAdminPath(''), targetPath));
await logEvent(
'admin_upload_save',
'admin',
{ upload_id: uploadEntry.id, target: relativeTarget },
await getRequestMeta()
);
redirectWithSuccess(dashboardPath, 'Upload im Dateimanager gespeichert.');
}
export async function adminMkdirAction(formData) {
@@ -565,19 +688,19 @@ export async function adminMkdirAction(formData) {
const folderName = String(formData.get('name') || '').trim();
if (!isValidNodeName(folderName)) {
redirectWithError(adminFilesHref(relativePath), 'Ungültiger Ordnername.');
redirectWithError(adminFilesTabHref(relativePath, 'folder'), 'Ungültiger Ordnername.');
}
const basePath = resolveAdminPath(relativePath);
if (!basePath) {
redirectWithError(adminFilesHref(relativePath), 'Ungültiger Pfad.');
redirectWithError(adminFilesTabHref(relativePath, 'folder'), 'Ungültiger Pfad.');
}
const targetPath = path.join(basePath, folderName);
try {
await fs.promises.mkdir(targetPath, { recursive: true });
} catch {
redirectWithError(adminFilesHref(relativePath), 'Ordner konnte nicht erstellt werden.');
redirectWithError(adminFilesTabHref(relativePath, 'folder'), 'Ordner konnte nicht erstellt werden.');
}
await logEvent(
@@ -587,7 +710,7 @@ export async function adminMkdirAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(adminFilesHref(relativePath), 'Ordner erstellt.');
redirectWithSuccess(adminFilesTabHref(relativePath, 'content'), 'Ordner erstellt.');
}
export async function adminUploadToPathAction(formData) {
@@ -602,30 +725,30 @@ export async function adminUploadToPathAction(formData) {
const relativePath = sanitizeRelativePath(formData.get('path'));
const basePath = resolveAdminPath(relativePath);
if (!basePath) {
redirectWithError(adminFilesHref(relativePath), 'Ungültiger Pfad.');
redirectWithError(adminFilesTabHref(relativePath, 'upload'), 'Ungültiger Pfad.');
}
const uploadedFile = uploadedFileFromForm(formData, 'file');
if (!uploadedFile || Number(uploadedFile.size || 0) <= 0) {
redirectWithError(adminFilesHref(relativePath), 'Keine Datei hochgeladen.');
redirectWithError(adminFilesTabHref(relativePath, 'upload'), 'Keine Datei hochgeladen.');
}
if (maxUploadBytes > 0 && Number(uploadedFile.size || 0) > maxUploadBytes) {
redirectWithError(
adminFilesHref(relativePath),
adminFilesTabHref(relativePath, 'upload'),
`Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`
);
}
const fileName = safeBaseName(uploadedFile.name, 'upload');
if (!isValidNodeName(fileName)) {
redirectWithError(adminFilesHref(relativePath), 'Ungültiger Dateiname.');
redirectWithError(adminFilesTabHref(relativePath, 'upload'), 'Ungültiger Dateiname.');
}
try {
await writeUploadedFile(uploadedFile, path.join(basePath, fileName));
} catch {
redirectWithError(adminFilesHref(relativePath), 'Datei konnte nicht hochgeladen werden.');
redirectWithError(adminFilesTabHref(relativePath, 'upload'), 'Datei konnte nicht hochgeladen werden.');
}
await logEvent(
@@ -635,7 +758,7 @@ export async function adminUploadToPathAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(adminFilesHref(relativePath), 'Datei hochgeladen.');
redirectWithSuccess(adminFilesTabHref(relativePath, 'content'), 'Datei hochgeladen.');
}
export async function adminRenamePathAction(formData) {
@@ -655,7 +778,7 @@ export async function adminRenamePathAction(formData) {
}
if (!isValidNodeName(newName)) {
redirectWithError(adminFilesHref(parentRelativePath(relativePath)), 'Ungültiger Name.');
redirectWithError(adminFilesTabHref(parentRelativePath(relativePath), 'content'), 'Ungültiger Name.');
}
const sourcePath = resolveAdminPath(relativePath);
@@ -667,7 +790,10 @@ export async function adminRenamePathAction(formData) {
try {
await fs.promises.rename(sourcePath, targetPath);
} catch {
redirectWithError(adminFilesHref(parentRelativePath(relativePath)), 'Pfad konnte nicht umbenannt werden.');
redirectWithError(
adminFilesTabHref(parentRelativePath(relativePath), 'content'),
'Pfad konnte nicht umbenannt werden.'
);
}
await logEvent(
@@ -680,7 +806,7 @@ export async function adminRenamePathAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(adminFilesHref(parentRelativePath(relativePath)), 'Pfad umbenannt.');
redirectWithSuccess(adminFilesTabHref(parentRelativePath(relativePath), 'content'), 'Pfad umbenannt.');
}
export async function adminDeletePathAction(formData) {
@@ -705,11 +831,14 @@ export async function adminDeletePathAction(formData) {
try {
await fs.promises.rm(sourcePath, { recursive: true, force: true });
} catch {
redirectWithError(adminFilesHref(parentRelativePath(relativePath)), 'Pfad konnte nicht gelöscht werden.');
redirectWithError(
adminFilesTabHref(parentRelativePath(relativePath), 'content'),
'Pfad konnte nicht gelöscht werden.'
);
}
await logEvent('admin_delete', 'admin', { path: relativePath }, await getRequestMeta());
redirectWithSuccess(adminFilesHref(parentRelativePath(relativePath)), 'Pfad gelöscht.');
redirectWithSuccess(adminFilesTabHref(parentRelativePath(relativePath), 'content'), 'Pfad gelöscht.');
}
export async function adminMovePathAction(formData) {
@@ -726,13 +855,13 @@ export async function adminMovePathAction(formData) {
const targetRelativePath = sanitizeRelativePath(formData.get('targetPath'));
if (!sourceRelativePath || !targetRelativePath) {
redirectWithError(adminFilesHref(currentPath), 'Ungültige Eingabe.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Ungültige Eingabe.');
}
const sourcePath = resolveAdminPath(sourceRelativePath);
const targetBasePath = resolveAdminPath(targetRelativePath);
if (!sourcePath || !targetBasePath) {
redirectWithError(adminFilesHref(currentPath), 'Ungültiger Pfad.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Ungültiger Pfad.');
}
const targetPath = await resolveMoveCopyTarget(sourcePath, targetBasePath);
@@ -745,10 +874,10 @@ export async function adminMovePathAction(formData) {
await copyPath(sourcePath, targetPath);
await fs.promises.rm(sourcePath, { recursive: true, force: true });
} catch {
redirectWithError(adminFilesHref(currentPath), 'Pfad konnte nicht verschoben werden.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Pfad konnte nicht verschoben werden.');
}
} else {
redirectWithError(adminFilesHref(currentPath), 'Pfad konnte nicht verschoben werden.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Pfad konnte nicht verschoben werden.');
}
}
@@ -759,7 +888,7 @@ export async function adminMovePathAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(adminFilesHref(currentPath), 'Pfad verschoben.');
redirectWithSuccess(adminFilesTabHref(currentPath, 'content'), 'Pfad verschoben.');
}
export async function adminCopyPathAction(formData) {
@@ -776,13 +905,13 @@ export async function adminCopyPathAction(formData) {
const targetRelativePath = sanitizeRelativePath(formData.get('targetPath'));
if (!sourceRelativePath || !targetRelativePath) {
redirectWithError(adminFilesHref(currentPath), 'Ungültige Eingabe.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Ungültige Eingabe.');
}
const sourcePath = resolveAdminPath(sourceRelativePath);
const targetBasePath = resolveAdminPath(targetRelativePath);
if (!sourcePath || !targetBasePath) {
redirectWithError(adminFilesHref(currentPath), 'Ungültiger Pfad.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Ungültiger Pfad.');
}
const targetPath = await resolveMoveCopyTarget(sourcePath, targetBasePath);
@@ -790,7 +919,7 @@ export async function adminCopyPathAction(formData) {
try {
await copyPath(sourcePath, targetPath);
} catch {
redirectWithError(adminFilesHref(currentPath), 'Pfad konnte nicht kopiert werden.');
redirectWithError(adminFilesTabHref(currentPath, 'content'), 'Pfad konnte nicht kopiert werden.');
}
await logEvent(
@@ -800,5 +929,5 @@ export async function adminCopyPathAction(formData) {
await getRequestMeta()
);
redirectWithSuccess(adminFilesHref(currentPath), 'Pfad kopiert.');
redirectWithSuccess(adminFilesTabHref(currentPath, 'content'), 'Pfad kopiert.');
}

View File

@@ -29,7 +29,7 @@ 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 maxUploadBytes = parseInteger(process.env.UPLOAD_MAX_BYTES || '26843545600', 26843545600);
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();

View File

@@ -51,7 +51,8 @@ function initializeSchema(database) {
database.run(`CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
created_at INTEGER NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0
)`);
database.run(`CREATE TABLE IF NOT EXISTS upload_requests (
id TEXT PRIMARY KEY,
@@ -74,6 +75,7 @@ function initializeSchema(database) {
database.run('ALTER TABLE uploads ADD COLUMN downloads INTEGER DEFAULT 0', () => undefined);
database.run('ALTER TABLE admin_logs ADD COLUMN ip TEXT', () => undefined);
database.run('ALTER TABLE admin_logs ADD COLUMN user_agent TEXT', () => undefined);
database.run('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0', () => undefined);
database.run('ALTER TABLE upload_requests ADD COLUMN upload_id INTEGER', () => undefined);
database.run('ALTER TABLE upload_requests ADD COLUMN uploaded_original_name TEXT', () => undefined);
database.run('ALTER TABLE upload_requests ADD COLUMN uploaded_stored_name TEXT', () => undefined);

View File

@@ -1,5 +1,6 @@
import fs from 'node:fs';
import { Readable } from 'node:stream';
import { Readable, Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import Busboy from 'busboy';
@@ -108,3 +109,43 @@ export async function streamMultipartToPath(request, options) {
Readable.fromWeb(request.body).pipe(parser);
});
}
export async function streamBodyToPath(request, options) {
const { targetPath, maxBytes = 0 } = options;
if (!request.body) {
throw new Error('missing-body');
}
let sizeBytes = 0;
const counter = new Transform({
transform(chunk, encoding, callback) {
sizeBytes += chunk.length;
if (maxBytes > 0 && sizeBytes > maxBytes) {
callback(new Error('file-too-large'));
return;
}
callback(null, chunk);
},
});
try {
await pipeline(
Readable.fromWeb(request.body),
counter,
fs.createWriteStream(targetPath, { flags: 'wx' })
);
} catch (error) {
await fs.promises.rm(targetPath, { force: true }).catch(() => undefined);
throw error;
}
if (sizeBytes <= 0) {
await fs.promises.rm(targetPath, { force: true }).catch(() => undefined);
throw new Error('missing-file');
}
return { sizeBytes };
}

View File

@@ -5,6 +5,7 @@ import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { cookieSecure } from './config.js';
import { get } from './db.js';
const state = globalThis.__filesLehnertSecurityState || (globalThis.__filesLehnertSecurityState = {});
@@ -131,9 +132,20 @@ export async function getAuthenticatedUser() {
if (!payload || typeof payload !== 'object' || !payload.sub) {
return null;
}
const username = String(payload.sub);
const authType = String(payload.auth || (username === 'admin' && payload.admin ? 'admin' : 'user'));
if (authType === 'admin' && payload.admin) {
return { username, admin: true };
}
const row = await get('SELECT is_admin FROM users WHERE username = ?', [username]);
if (!row) {
return null;
}
return {
username: String(payload.sub),
admin: Boolean(payload.admin),
username,
admin: Boolean(row.is_admin),
};
} catch {
return null;