added requesting option
This commit is contained in:
@@ -369,6 +369,54 @@ export async function extendOwnUploadAction(formData) {
|
||||
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Aufbewahrung aktualisiert.');
|
||||
}
|
||||
|
||||
export async function createUploadRequestAction(formData) {
|
||||
await runCleanupIfNeeded();
|
||||
|
||||
try {
|
||||
await verifyCsrf(formData);
|
||||
} catch {
|
||||
redirectWithError(`${managementBasePath}/dashboard`, 'CSRF-Prüfung fehlgeschlagen.');
|
||||
}
|
||||
|
||||
const user = await requireAuthenticatedUser();
|
||||
const note = String(formData.get('note') || '').trim().slice(0, 500);
|
||||
const now = Date.now();
|
||||
const expiresSeconds = parseHours(formData.get('expiresHours'), 72 * 3600);
|
||||
const expiresAt = Math.min(now + expiresSeconds * 1000, now + maxRetentionSeconds * 1000);
|
||||
|
||||
let requestId = '';
|
||||
for (let attempts = 0; attempts < 12; attempts += 1) {
|
||||
const candidate = createRandomId();
|
||||
const existing = await get('SELECT id FROM upload_requests WHERE id = ?', [candidate]);
|
||||
if (!existing) {
|
||||
requestId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestId) {
|
||||
redirectWithError(`${managementBasePath}/dashboard`, 'Anfrage-ID konnte nicht erzeugt werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
await run(
|
||||
'INSERT INTO upload_requests (id, owner, note, created_at, expires_at) VALUES (?, ?, ?, ?, ?)',
|
||||
[requestId, user.username, note || null, now, expiresAt]
|
||||
);
|
||||
} catch {
|
||||
redirectWithError(`${managementBasePath}/dashboard`, 'Anfrage konnte nicht gespeichert werden.');
|
||||
}
|
||||
|
||||
await logEvent(
|
||||
'request_create',
|
||||
user.username,
|
||||
{ request_id: requestId, expires_at: expiresAt, note: note || null },
|
||||
await getRequestMeta()
|
||||
);
|
||||
|
||||
redirectWithSuccess(`${managementBasePath}/dashboard`, 'Upload-Anfrage erstellt.');
|
||||
}
|
||||
|
||||
export async function adminCreateUserAction(formData) {
|
||||
try {
|
||||
await verifyCsrf(formData);
|
||||
|
||||
@@ -14,3 +14,9 @@ export const uploadTtlSeconds = parseInteger(process.env.UPLOAD_TTL_SECONDS || '
|
||||
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 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();
|
||||
export const smtpPass = String(process.env.SMTP_PASS || '');
|
||||
export const smtpMail = String(process.env.SMTP_MAIL || '').trim();
|
||||
export const smtpName = String(process.env.SMTP_NAME || '').trim();
|
||||
|
||||
@@ -53,9 +53,34 @@ function initializeSchema(database) {
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)`);
|
||||
database.run(`CREATE TABLE IF NOT EXISTS upload_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
note TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
completed_at INTEGER,
|
||||
upload_id INTEGER,
|
||||
uploaded_original_name TEXT,
|
||||
uploaded_stored_name TEXT,
|
||||
uploaded_stored_path TEXT,
|
||||
uploaded_size_bytes INTEGER,
|
||||
fulfilled_by TEXT,
|
||||
notification_sent_at INTEGER
|
||||
)`);
|
||||
database.run('CREATE INDEX IF NOT EXISTS upload_requests_owner_idx ON upload_requests(owner)');
|
||||
database.run('CREATE INDEX IF NOT EXISTS upload_requests_expires_idx ON upload_requests(expires_at)');
|
||||
database.run('CREATE INDEX IF NOT EXISTS upload_requests_completed_idx ON upload_requests(completed_at)');
|
||||
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 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);
|
||||
database.run('ALTER TABLE upload_requests ADD COLUMN uploaded_stored_path TEXT', () => undefined);
|
||||
database.run('ALTER TABLE upload_requests ADD COLUMN uploaded_size_bytes INTEGER', () => undefined);
|
||||
database.run('ALTER TABLE upload_requests ADD COLUMN fulfilled_by TEXT', () => undefined);
|
||||
database.run('ALTER TABLE upload_requests ADD COLUMN notification_sent_at INTEGER', () => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
110
nextjs/src/lib/mailer.js
Normal file
110
nextjs/src/lib/mailer.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import {
|
||||
smtpHost,
|
||||
smtpMail,
|
||||
smtpName,
|
||||
smtpPass,
|
||||
smtpPort,
|
||||
smtpUser,
|
||||
} from './config.js';
|
||||
|
||||
const state = globalThis.__filesLehnertMailerState || (globalThis.__filesLehnertMailerState = {});
|
||||
|
||||
function isSmtpConfigured() {
|
||||
return Boolean(smtpHost && smtpPort > 0 && smtpUser && smtpPass && smtpMail);
|
||||
}
|
||||
|
||||
function senderAddress() {
|
||||
if (!smtpMail) {
|
||||
return '';
|
||||
}
|
||||
return smtpName ? `${smtpName} <${smtpMail}>` : smtpMail;
|
||||
}
|
||||
|
||||
function smtpTransport() {
|
||||
if (!isSmtpConfigured()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!state.transporter) {
|
||||
state.transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return state.transporter;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export async function sendUploadRequestCompletedMail({
|
||||
recipient,
|
||||
requestId,
|
||||
fileName,
|
||||
downloadUrl,
|
||||
requesterNote,
|
||||
fulfilledBy,
|
||||
}) {
|
||||
const transporter = smtpTransport();
|
||||
if (!transporter) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'smtp-not-configured',
|
||||
};
|
||||
}
|
||||
|
||||
const noteText = requesterNote ? `Notiz: ${requesterNote}\n` : '';
|
||||
const fulfilledByText = fulfilledBy ? `Hochgeladen von: ${fulfilledBy}\n` : '';
|
||||
const text = [
|
||||
'Eine Upload-Anfrage wurde abgeschlossen.',
|
||||
'',
|
||||
`Anfrage-ID: ${requestId}`,
|
||||
`Datei: ${fileName}`,
|
||||
fulfilledByText.trim(),
|
||||
noteText.trim(),
|
||||
`Download: ${downloadUrl}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
const htmlLines = [
|
||||
'<p>Eine Upload-Anfrage wurde abgeschlossen.</p>',
|
||||
`<p><strong>Anfrage-ID:</strong> ${escapeHtml(requestId)}</p>`,
|
||||
`<p><strong>Datei:</strong> ${escapeHtml(fileName)}</p>`,
|
||||
fulfilledBy ? `<p><strong>Hochgeladen von:</strong> ${escapeHtml(fulfilledBy)}</p>` : '',
|
||||
requesterNote ? `<p><strong>Notiz:</strong> ${escapeHtml(requesterNote)}</p>` : '',
|
||||
`<p><a href="${escapeHtml(downloadUrl)}">Download öffnen</a></p>`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: senderAddress(),
|
||||
to: recipient,
|
||||
subject: `Upload-Anfrage ${requestId} abgeschlossen`,
|
||||
text,
|
||||
html: htmlLines,
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: error && error.message ? error.message : 'smtp-send-failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user