Upload-Freigabe: öffentlicher Upload-Link mit Passwort, Ablaufzeit, max. Größe; Admin-Verwaltung
This commit is contained in:
@@ -521,4 +521,46 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='cal
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Upload-Freigaben (externer Upload-Link) ───────────────────────────────────
|
||||||
|
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='upload_shares'").get()) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE upload_shares (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
max_size_mb INTEGER NOT NULL DEFAULT 10,
|
||||||
|
folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='upload_share_logs'").get()) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE upload_share_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
share_id INTEGER NOT NULL REFERENCES upload_shares(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
originalname TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ip_address TEXT NOT NULL DEFAULT '',
|
||||||
|
uploaded_at DATETIME DEFAULT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
// allow_upload_share Recht pro User
|
||||||
|
{
|
||||||
|
const userCols = db.pragma('table_info(users)').map(c => c.name);
|
||||||
|
if (!userCols.includes('allow_upload_share'))
|
||||||
|
db.exec('ALTER TABLE users ADD COLUMN allow_upload_share INTEGER DEFAULT 0');
|
||||||
|
}
|
||||||
|
// Upload-Ordner Flag auf Folders-Tabelle
|
||||||
|
{
|
||||||
|
const folderCols = db.pragma('table_info(folders)').map(c => c.name);
|
||||||
|
if (!folderCols.includes('is_upload_folder'))
|
||||||
|
db.exec('ALTER TABLE folders ADD COLUMN is_upload_folder INTEGER DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ fs.readdirSync(toolsDir, { withFileTypes: true })
|
|||||||
|
|
||||||
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
|
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
|
||||||
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
|
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
|
||||||
|
app.use('/api/upload-shares', require('./tools/dateien/upload-share'));
|
||||||
app.use('/api/search', require('./routes/search'));
|
app.use('/api/search', require('./routes/search'));
|
||||||
|
|
||||||
const PUBLIC = path.join(__dirname, '../public');
|
const PUBLIC = path.join(__dirname, '../public');
|
||||||
@@ -84,6 +85,13 @@ app.get('/sw.js', (_req, res) => {
|
|||||||
res.sendFile(path.join(PUBLIC, 'sw.js'));
|
res.sendFile(path.join(PUBLIC, 'sw.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Öffentliche Upload-Seite (kein Login erforderlich)
|
||||||
|
const UPLOAD_HTML = path.join(__dirname, '../upload-page.html');
|
||||||
|
app.get('/u/:token', (_req, res) => {
|
||||||
|
if (require('fs').existsSync(UPLOAD_HTML)) res.sendFile(UPLOAD_HTML);
|
||||||
|
else res.status(404).send('Upload-Seite nicht gefunden');
|
||||||
|
});
|
||||||
|
|
||||||
app.use(express.static(PUBLIC, {
|
app.use(express.static(PUBLIC, {
|
||||||
setHeaders: (res, filePath) => {
|
setHeaders: (res, filePath) => {
|
||||||
// JS/CSS Assets haben Hash im Namen → lang cachen
|
// JS/CSS Assets haben Hash im Namen → lang cachen
|
||||||
|
|||||||
183
backend/src/tools/dateien/upload-share.js
Normal file
183
backend/src/tools/dateien/upload-share.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const db = require('../../db');
|
||||||
|
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
|
||||||
|
|
||||||
|
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
|
||||||
|
function generateToken() {
|
||||||
|
return crypto.randomBytes(20).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateUploadFolder(userId) {
|
||||||
|
let folder = db.prepare(
|
||||||
|
"SELECT * FROM folders WHERE user_id=? AND is_upload_folder=1 LIMIT 1"
|
||||||
|
).get(userId);
|
||||||
|
if (!folder) {
|
||||||
|
const r = db.prepare(
|
||||||
|
"INSERT INTO folders (user_id, name, is_upload_folder, created_at) VALUES (?,?,1,datetime('now','localtime'))"
|
||||||
|
).run(userId, 'Upload-Ordner');
|
||||||
|
folder = db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid);
|
||||||
|
}
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseUploadShare(userId) {
|
||||||
|
const user = db.prepare('SELECT role, allow_upload_share FROM users WHERE id=?').get(userId);
|
||||||
|
return user && (user.role === 'admin' || !!user.allow_upload_share);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eigene Shares verwalten ───────────────────────────────────────────────────
|
||||||
|
router.get('/', authenticate, (req, res) => {
|
||||||
|
if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error: 'Keine Berechtigung' });
|
||||||
|
const folder = getOrCreateUploadFolder(req.user.id);
|
||||||
|
const shares = db.prepare(`
|
||||||
|
SELECT s.*, COUNT(l.id) as upload_count
|
||||||
|
FROM upload_shares s
|
||||||
|
LEFT JOIN upload_share_logs l ON l.share_id=s.id
|
||||||
|
WHERE s.user_id=?
|
||||||
|
GROUP BY s.id
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
`).all(req.user.id);
|
||||||
|
res.json({ shares, folder });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', authenticate, (req, res) => {
|
||||||
|
if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error: 'Keine Berechtigung' });
|
||||||
|
const { password, expires_hours=24, max_size_mb=10 } = req.body;
|
||||||
|
if (!password?.trim()) return res.status(400).json({ error: 'Passwort erforderlich' });
|
||||||
|
const folder = getOrCreateUploadFolder(req.user.id);
|
||||||
|
const token = generateToken();
|
||||||
|
const hash = bcrypt.hashSync(password.trim(), 10);
|
||||||
|
const expires = new Date(Date.now() + Number(expires_hours) * 3600000).toISOString();
|
||||||
|
const r = db.prepare(`
|
||||||
|
INSERT INTO upload_shares (user_id, token, password_hash, expires_at, max_size_mb, folder_id, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
|
`).run(req.user.id, token, hash, expires, Number(max_size_mb), folder.id);
|
||||||
|
res.json(db.prepare('SELECT * FROM upload_shares WHERE id=?').get(r.lastInsertRowid));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', authenticate, (req, res) => {
|
||||||
|
const s = db.prepare('SELECT * FROM upload_shares WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||||
|
if (!s) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||||
|
db.prepare('UPDATE upload_shares SET is_active=0 WHERE id=?').run(s.id);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/logs', authenticate, (req, res) => {
|
||||||
|
if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error: 'Keine Berechtigung' });
|
||||||
|
const logs = db.prepare(`
|
||||||
|
SELECT l.*, s.expires_at, s.max_size_mb
|
||||||
|
FROM upload_share_logs l
|
||||||
|
JOIN upload_shares s ON s.id=l.share_id
|
||||||
|
WHERE l.user_id=?
|
||||||
|
ORDER BY l.uploaded_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
`).all(req.user.id);
|
||||||
|
res.json(logs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Admin: alle Logs sehen ────────────────────────────────────────────────────
|
||||||
|
router.get('/admin/logs', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const logs = db.prepare(`
|
||||||
|
SELECT l.*, s.expires_at, s.max_size_mb, u.username
|
||||||
|
FROM upload_share_logs l
|
||||||
|
JOIN upload_shares s ON s.id=l.share_id
|
||||||
|
JOIN users u ON u.id=l.user_id
|
||||||
|
ORDER BY l.uploaded_at DESC
|
||||||
|
LIMIT 500
|
||||||
|
`).all();
|
||||||
|
res.json(logs);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/admin/shares', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const shares = db.prepare(`
|
||||||
|
SELECT s.*, u.username, COUNT(l.id) as upload_count
|
||||||
|
FROM upload_shares s
|
||||||
|
JOIN users u ON u.id=s.user_id
|
||||||
|
LEFT JOIN upload_share_logs l ON l.share_id=s.id
|
||||||
|
GROUP BY s.id
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
`).all();
|
||||||
|
res.json(shares);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Admin: Berechtigung pro User setzen ──────────────────────────────────────
|
||||||
|
router.post('/admin/permission', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const { user_id, allow } = req.body;
|
||||||
|
db.prepare('UPDATE users SET allow_upload_share=? WHERE id=?').run(allow?1:0, user_id);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Öffentlicher Endpunkt: Infos zum Share ────────────────────────────────────
|
||||||
|
router.get('/public/:token', (req, res) => {
|
||||||
|
const s = db.prepare(
|
||||||
|
"SELECT id, expires_at, max_size_mb, is_active FROM upload_shares WHERE token=?"
|
||||||
|
).get(req.params.token);
|
||||||
|
if (!s || !s.is_active) return res.status(404).json({ error: 'Link ungültig oder deaktiviert' });
|
||||||
|
if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error: 'Link abgelaufen' });
|
||||||
|
res.json({ ok: true, max_size_mb: s.max_size_mb, expires_at: s.expires_at });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Öffentlicher Endpunkt: Passwort prüfen ────────────────────────────────────
|
||||||
|
router.post('/public/:token/verify', (req, res) => {
|
||||||
|
const s = db.prepare('SELECT * FROM upload_shares WHERE token=?').get(req.params.token);
|
||||||
|
if (!s || !s.is_active) return res.status(404).json({ error: 'Link ungültig' });
|
||||||
|
if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error: 'Link abgelaufen' });
|
||||||
|
if (!bcrypt.compareSync(req.body.password || '', s.password_hash))
|
||||||
|
return res.status(401).json({ error: 'Falsches Passwort' });
|
||||||
|
res.json({ ok: true, max_size_mb: s.max_size_mb, expires_at: s.expires_at });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Öffentlicher Upload ───────────────────────────────────────────────────────
|
||||||
|
router.post('/public/:token/upload', (req, res) => {
|
||||||
|
const s = db.prepare('SELECT * FROM upload_shares WHERE token=?').get(req.params.token);
|
||||||
|
if (!s || !s.is_active) return res.status(404).json({ error: 'Link ungültig' });
|
||||||
|
if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error: 'Link abgelaufen' });
|
||||||
|
|
||||||
|
// Passwort im Header prüfen
|
||||||
|
const pw = req.headers['x-upload-password'] || '';
|
||||||
|
if (!bcrypt.compareSync(pw, s.password_hash))
|
||||||
|
return res.status(401).json({ error: 'Nicht autorisiert' });
|
||||||
|
|
||||||
|
const maxBytes = s.max_size_mb * 1024 * 1024;
|
||||||
|
const storage = multer.diskStorage({ destination: UPLOAD_DIR });
|
||||||
|
const upload = multer({
|
||||||
|
storage,
|
||||||
|
limits: { fileSize: maxBytes },
|
||||||
|
}).single('file');
|
||||||
|
|
||||||
|
upload(req, res, err => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'LIMIT_FILE_SIZE')
|
||||||
|
return res.status(413).json({ error: `Datei zu groß (max ${s.max_size_mb} MB)` });
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
if (!req.file) return res.status(400).json({ error: 'Keine Datei' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = db.prepare(`
|
||||||
|
INSERT INTO files (user_id, filename, originalname, mimetype, size, folder_id, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
|
`).run(s.user_id, req.file.filename, req.file.originalname, req.file.mimetype, req.file.size, s.folder_id);
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO upload_share_logs (share_id, user_id, originalname, size, ip_address, uploaded_at)
|
||||||
|
VALUES (?,?,?,?,?,datetime('now','localtime'))
|
||||||
|
`).run(s.id, s.user_id, req.file.originalname, req.file.size, req.ip || '');
|
||||||
|
|
||||||
|
res.json({ ok: true, filename: req.file.originalname });
|
||||||
|
} catch(e) {
|
||||||
|
try { fs.unlinkSync(path.join(UPLOAD_DIR, req.file.filename)); } catch {}
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
147
backend/upload-page.html
Normal file
147
backend/upload-page.html
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Datei hochladen – DickenDock</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{background:#0d0d0f;color:#fff;font-family:'Courier New',monospace;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
|
||||||
|
.card{background:#1a1a1e;border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:32px;width:100%;max-width:440px}
|
||||||
|
h1{font-size:18px;margin-bottom:6px;color:#4ecdc4}
|
||||||
|
.sub{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:24px}
|
||||||
|
label{display:block;color:rgba(255,255,255,0.5);font-size:10px;letter-spacing:2px;margin-bottom:4px}
|
||||||
|
input[type=password],input[type=file]{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:10px 12px;color:#fff;font-family:inherit;font-size:14px;outline:none}
|
||||||
|
input[type=file]{cursor:pointer;padding:8px}
|
||||||
|
input[type=file]::file-selector-button{background:#4ecdc4;border:none;border-radius:6px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-weight:700;padding:4px 12px;margin-right:10px}
|
||||||
|
.btn{width:100%;background:#4ecdc4;border:none;border-radius:8px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:12px;margin-top:12px}
|
||||||
|
.btn:disabled{opacity:0.4;cursor:default}
|
||||||
|
.btn.danger{background:#ff6b9d}
|
||||||
|
.info{background:rgba(78,205,196,0.08);border:1px solid rgba(78,205,196,0.2);border-radius:8px;padding:10px 12px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:16px}
|
||||||
|
.msg{border-radius:8px;padding:10px 12px;font-size:13px;margin-top:12px}
|
||||||
|
.msg.ok{background:rgba(78,205,196,0.12);border:1px solid rgba(78,205,196,0.3);color:#4ecdc4}
|
||||||
|
.msg.err{background:rgba(255,107,157,0.12);border:1px solid rgba(255,107,157,0.3);color:#ff6b9d}
|
||||||
|
.hidden{display:none}
|
||||||
|
.done-list{margin-top:12px;font-size:12px;color:rgba(255,255,255,0.5)}
|
||||||
|
.done-list li{padding:3px 0;list-style:none}
|
||||||
|
.done-list li::before{content:'✓ ';color:#4ecdc4}
|
||||||
|
#progress{height:4px;background:rgba(78,205,196,0.2);border-radius:2px;margin-top:8px;overflow:hidden}
|
||||||
|
#progress-bar{height:100%;background:#4ecdc4;width:0;transition:width 0.3s}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>📤 Datei-Upload</h1>
|
||||||
|
<p class="sub" id="sub">Dateien hochladen</p>
|
||||||
|
|
||||||
|
<!-- Passwort-Sektion -->
|
||||||
|
<div id="pw-section">
|
||||||
|
<div class="info" id="share-info">Link wird geprüft…</div>
|
||||||
|
<label>PASSWORT</label>
|
||||||
|
<input type="password" id="pw" placeholder="Passwort eingeben" autocomplete="off"/>
|
||||||
|
<button class="btn" id="pw-btn" onclick="verify()">Zugang bestätigen</button>
|
||||||
|
<div id="pw-msg" class="msg hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload-Sektion -->
|
||||||
|
<div id="upload-section" class="hidden">
|
||||||
|
<div class="info" id="upload-info"></div>
|
||||||
|
<label>DATEI AUSWÄHLEN</label>
|
||||||
|
<input type="file" id="file-input" multiple/>
|
||||||
|
<div id="progress"><div id="progress-bar"></div></div>
|
||||||
|
<button class="btn" id="upload-btn" onclick="upload()">⬆ Hochladen</button>
|
||||||
|
<div id="upload-msg" class="msg hidden"></div>
|
||||||
|
<ul class="done-list" id="done-list"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const token = location.pathname.replace('/u/','').replace('/','');
|
||||||
|
let password = '';
|
||||||
|
let maxMb = 10;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/upload-shares/public/${token}`);
|
||||||
|
const d = await r.json();
|
||||||
|
if (!r.ok) return showFatal(d.error || 'Ungültiger Link');
|
||||||
|
maxMb = d.max_size_mb;
|
||||||
|
const exp = new Date(d.expires_at).toLocaleString('de-DE', {timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||||
|
document.getElementById('share-info').textContent = `Gültig bis: ${exp} · Max ${d.max_size_mb} MB pro Datei`;
|
||||||
|
} catch { showFatal('Verbindung fehlgeschlagen'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFatal(msg) {
|
||||||
|
document.getElementById('pw-section').innerHTML = `<div class="msg err">${msg}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verify() {
|
||||||
|
const pw = document.getElementById('pw').value;
|
||||||
|
if (!pw) return;
|
||||||
|
document.getElementById('pw-btn').disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/upload-shares/public/${token}/verify`, {
|
||||||
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ password: pw })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (!r.ok) {
|
||||||
|
document.getElementById('pw-msg').className = 'msg err';
|
||||||
|
document.getElementById('pw-msg').textContent = d.error;
|
||||||
|
document.getElementById('pw-msg').classList.remove('hidden');
|
||||||
|
document.getElementById('pw-btn').disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
password = pw;
|
||||||
|
maxMb = d.max_size_mb;
|
||||||
|
document.getElementById('pw-section').classList.add('hidden');
|
||||||
|
document.getElementById('upload-section').classList.remove('hidden');
|
||||||
|
const exp = new Date(d.expires_at).toLocaleString('de-DE', {timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||||
|
document.getElementById('upload-info').textContent = `Gültig bis ${exp} · Max ${d.max_size_mb} MB pro Datei`;
|
||||||
|
document.getElementById('sub').textContent = 'Dateien hochladen ✓';
|
||||||
|
} catch {
|
||||||
|
document.getElementById('pw-btn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload() {
|
||||||
|
const files = document.getElementById('file-input').files;
|
||||||
|
if (!files.length) return;
|
||||||
|
const btn = document.getElementById('upload-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
const msg = document.getElementById('upload-msg');
|
||||||
|
const bar = document.getElementById('progress-bar');
|
||||||
|
const list = document.getElementById('done-list');
|
||||||
|
|
||||||
|
for (let i=0; i<files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (file.size > maxMb*1024*1024) {
|
||||||
|
msg.className = 'msg err'; msg.textContent = `"${file.name}" ist zu groß (max ${maxMb} MB)`; msg.classList.remove('hidden');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bar.style.width = ((i+0.5)/files.length*100)+'%';
|
||||||
|
const fd = new FormData(); fd.append('file', file);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/upload-shares/public/${token}/upload`, {
|
||||||
|
method:'POST', headers:{'x-upload-password': password}, body: fd
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (r.ok) {
|
||||||
|
const li = document.createElement('li'); li.textContent = file.name; list.appendChild(li);
|
||||||
|
msg.className = 'msg ok'; msg.textContent = `${list.children.length} Datei(en) erfolgreich hochgeladen`; msg.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
msg.className = 'msg err'; msg.textContent = d.error; msg.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch { msg.className='msg err'; msg.textContent='Upload fehlgeschlagen'; msg.classList.remove('hidden'); }
|
||||||
|
}
|
||||||
|
bar.style.width = '100%';
|
||||||
|
setTimeout(()=>{ bar.style.width='0'; }, 1500);
|
||||||
|
btn.disabled = false;
|
||||||
|
document.getElementById('file-input').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pw').addEventListener('keydown', e => { if(e.key==='Enter') verify(); });
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1481,7 +1481,6 @@ function SearchResultItem({ item, idx, activeIdx, onNavigate, onHover }) {
|
|||||||
if (item.type==='calendar_feed') { icon='📅'; primary=item.name; secondary='Kalender-Abo'; }
|
if (item.type==='calendar_feed') { icon='📅'; primary=item.name; secondary='Kalender-Abo'; }
|
||||||
if (item.type==='calendar_event') { icon='📆'; primary=item.summary; secondary=`${item.feed_name||'Kalender'}${item.start_dt?' · '+formatIcalDate(item.start_dt):''}`; }
|
if (item.type==='calendar_event') { icon='📆'; primary=item.summary; secondary=`${item.feed_name||'Kalender'}${item.start_dt?' · '+formatIcalDate(item.start_dt):''}`; }
|
||||||
if (item.type==='qr') { icon='◻'; primary=item.label||item.url; secondary=`QR-Code · ${item.url}`; }
|
if (item.type==='qr') { icon='◻'; primary=item.label||item.url; secondary=`QR-Code · ${item.url}`; }
|
||||||
if (item.type==='user') { icon='💬'; primary=item.username; secondary='Nachricht schreiben → Chat öffnen'; }
|
|
||||||
return (
|
return (
|
||||||
<div onClick={()=>onNavigate(item)} onMouseEnter={()=>onHover(idx)}
|
<div onClick={()=>onNavigate(item)} onMouseEnter={()=>onHover(idx)}
|
||||||
style={{display:'flex',alignItems:'center',gap:10,padding:'9px 14px',cursor:'pointer',
|
style={{display:'flex',alignItems:'center',gap:10,padding:'9px 14px',cursor:'pointer',
|
||||||
@@ -1643,7 +1642,6 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
|||||||
...(results.calendar_feeds||[]).map(f=>({...f,type:'calendar_feed'})),
|
...(results.calendar_feeds||[]).map(f=>({...f,type:'calendar_feed'})),
|
||||||
...(results.calendar_events||[]).map(e=>({...e,type:'calendar_event'})),
|
...(results.calendar_events||[]).map(e=>({...e,type:'calendar_event'})),
|
||||||
...(results.qr_codes||[]).map(q=>({...q,type:'qr'})),
|
...(results.qr_codes||[]).map(q=>({...q,type:'qr'})),
|
||||||
...(results.users||[]).map(u=>({...u,type:'user'})),
|
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
const navigate = (item) => {
|
const navigate = (item) => {
|
||||||
@@ -1663,8 +1661,7 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
|||||||
else if (item.type === 'file' || item.type === 'file_folder') { setActive('dateien'); }
|
else if (item.type === 'file' || item.type === 'file_folder') { setActive('dateien'); }
|
||||||
else if (item.type === 'push') { setActive('dashboard'); }
|
else if (item.type === 'push') { setActive('dashboard'); }
|
||||||
else if (item.type === 'calendar_feed' || item.type === 'calendar_event') { setActive('dashboard'); }
|
else if (item.type === 'calendar_feed' || item.type === 'calendar_event') { setActive('dashboard'); }
|
||||||
else if (item.type === 'qr') { setActive('devtools'); setDevToolsNav({tool:'qrcode', ts: Date.now()}); }
|
else if (item.type === 'qr') { setActive('devtools'); setDevToolsNav({tool:'qrcode', ts: Date.now()}); }
|
||||||
else if (item.type === 'user') { setActive('nachrichten'); setDevToolsNav({userId: item.id, ts: Date.now()}); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKey = e => {
|
const onKey = e => {
|
||||||
@@ -1803,12 +1800,6 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
|
|||||||
{results.qr_codes.map((item,i)=>(<SearchResultItem key={`qr${i}`} item={{...item,type:'qr'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
{results.qr_codes.map((item,i)=>(<SearchResultItem key={`qr${i}`} item={{...item,type:'qr'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{results.users?.length>0 && (
|
|
||||||
<div>
|
|
||||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>BENUTZER / CHAT</div>
|
|
||||||
{results.users.map((item,i)=>(<SearchResultItem key={`u${i}`} item={{...item,type:'user'}} idx={0} activeIdx={-1} onNavigate={navigate} onHover={()=>{}}/>))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{results.notes?.length>0 && (
|
{results.notes?.length>0 && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>NOTIZEN</div>
|
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>NOTIZEN</div>
|
||||||
@@ -2594,6 +2585,77 @@ const Field = ({ label, type='text', value, onChange, placeholder='', autoCapita
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Admin: Upload-Freigabe Berechtigungen ─────────────────────────────────────
|
||||||
|
function UploadShareAdminPanel({ toast }) {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
api('/users').then(setUsers).catch(()=>{});
|
||||||
|
};
|
||||||
|
const loadLogs = () => {
|
||||||
|
api('/upload-shares/admin/logs').then(setLogs).catch(()=>{});
|
||||||
|
setShowLogs(true);
|
||||||
|
};
|
||||||
|
useEffect(()=>{ load(); },[]);
|
||||||
|
|
||||||
|
const toggle = async (uid, current) => {
|
||||||
|
try {
|
||||||
|
await api('/upload-shares/admin/permission', { body:{ user_id: uid, allow: !current } });
|
||||||
|
setUsers(p => p.map(u => u.id===uid ? {...u, allow_upload_share: current?0:1} : u));
|
||||||
|
toast(current ? 'Berechtigung entzogen' : 'Berechtigung erteilt');
|
||||||
|
} catch(e) { toast(e.message,'error'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const fmtDate = d => d ? new Date(d).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '–';
|
||||||
|
const fmtSize = b => b>1024*1024 ? (b/1024/1024).toFixed(1)+' MB' : (b/1024).toFixed(0)+' KB';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:12,marginBottom:12,lineHeight:1.7}}>
|
||||||
|
Admins haben immer Zugriff auf Upload-Freigaben.<br/>
|
||||||
|
Hier kannst du die Funktion für einzelne Mitglieder freischalten.
|
||||||
|
</div>
|
||||||
|
{users.filter(u=>u.role!=='admin').map(u => (
|
||||||
|
<div key={u.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',borderBottom:'1px solid rgba(255,255,255,0.06)'}}>
|
||||||
|
<span style={{flex:1,color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:13}}>{u.username}</span>
|
||||||
|
<button onClick={()=>toggle(u.id, !!u.allow_upload_share)}
|
||||||
|
style={{
|
||||||
|
padding:'4px 14px',borderRadius:16,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||||||
|
background: u.allow_upload_share ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.05)',
|
||||||
|
border: `1px solid ${u.allow_upload_share ? '#4ecdc4' : 'rgba(255,255,255,0.15)'}`,
|
||||||
|
color: u.allow_upload_share ? '#4ecdc4' : 'rgba(255,255,255,0.4)',
|
||||||
|
}}>
|
||||||
|
{u.allow_upload_share ? '● Erlaubt' : '○ Gesperrt'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button onClick={loadLogs} style={{...S.btn('#ffe66d',true),marginTop:14,fontSize:11}}>
|
||||||
|
📋 Nutzungsprotokoll anzeigen
|
||||||
|
</button>
|
||||||
|
{showLogs && (
|
||||||
|
<div style={{marginTop:12}}>
|
||||||
|
<div style={{...S.head,marginBottom:8}}>ALLE UPLOADS ÜBER FREIGABE-LINKS</div>
|
||||||
|
{logs.length===0
|
||||||
|
? <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11}}>Noch keine Uploads.</div>
|
||||||
|
: logs.map(l=>(
|
||||||
|
<div key={l.id} style={{display:'flex',gap:10,padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)',flexWrap:'wrap'}}>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,flexShrink:0,minWidth:90}}>{l.username}</span>
|
||||||
|
<div style={{flex:1,minWidth:120}}>
|
||||||
|
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{l.originalname}</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10}}>{fmtDate(l.uploaded_at)} · {fmtSize(l.size)} · Max: {l.max_size_mb} MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function AdminPanel({ toast, mobile, user, nav }) {
|
function AdminPanel({ toast, mobile, user, nav }) {
|
||||||
const [section, setSection] = useState('profil');
|
const [section, setSection] = useState('profil');
|
||||||
useEffect(() => { if (nav?.section) setSection(nav.section); }, [nav?.ts]);
|
useEffect(() => { if (nav?.section) setSection(nav.section); }, [nav?.ts]);
|
||||||
@@ -2740,6 +2802,9 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
|||||||
<Sec title="DATEI-MANAGER LIMITS">
|
<Sec title="DATEI-MANAGER LIMITS">
|
||||||
<DateiSettings toast={toast}/>
|
<DateiSettings toast={toast}/>
|
||||||
</Sec>
|
</Sec>
|
||||||
|
<Sec title="UPLOAD-FREIGABE">
|
||||||
|
<UploadShareAdminPanel toast={toast}/>
|
||||||
|
</Sec>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2898,7 +2963,7 @@ export default function App() {
|
|||||||
<main style={mainStyle}>
|
<main style={mainStyle}>
|
||||||
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
|
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
|
||||||
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user} nav={devToolsNav}/>}
|
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user} nav={devToolsNav}/>}
|
||||||
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive} {...(activeTool.id==='devtools'||activeTool.id==='nachrichten'?{nav:devToolsNav}:{})}/>}
|
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive} {...(activeTool.id==='devtools'?{nav:devToolsNav}:{})}/>}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{mobile && (
|
{mobile && (
|
||||||
|
|||||||
@@ -158,6 +158,192 @@ function PasswordPromptModal({ filename, onSubmit, onCancel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Upload-Ordner & Share-Verwaltung ─────────────────────────────────────────
|
||||||
|
function UploadShareManager({ toast }) {
|
||||||
|
const [shares, setShares] = useState([]);
|
||||||
|
const [folder, setFolder] = useState(null);
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
|
const [form, setForm] = useState({ password:'', expires_hours:24, max_size_mb:10 });
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(null);
|
||||||
|
|
||||||
|
const load = () => api('/upload-shares').then(d=>{ setShares(d.shares||[]); setFolder(d.folder); }).catch(()=>{});
|
||||||
|
useEffect(()=>{ load(); },[]);
|
||||||
|
|
||||||
|
const genPw = () => {
|
||||||
|
const chars='ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%';
|
||||||
|
setForm(p=>({...p, password: Array.from({length:12},()=>chars[Math.floor(Math.random()*chars.length)]).join('')}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
if (!form.password.trim()) { toast('Passwort erforderlich','error'); return; }
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api('/upload-shares', { body: form });
|
||||||
|
toast('Upload-Link erstellt ✓'); setShowCreate(false); setForm({password:'',expires_hours:24,max_size_mb:10}); load();
|
||||||
|
} catch(e) { toast(e.message,'error'); }
|
||||||
|
setBusy(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivate = async id => {
|
||||||
|
await api(`/upload-shares/${id}`, { method:'DELETE' });
|
||||||
|
load(); toast('Link deaktiviert');
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLink = (token) => {
|
||||||
|
const url = `${window.location.origin}/u/${token}`;
|
||||||
|
navigator.clipboard?.writeText(url).then(()=>{ setCopied(token); setTimeout(()=>setCopied(null),2000); }).catch(()=>{
|
||||||
|
toast(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLogs = () => {
|
||||||
|
api('/upload-shares/logs').then(setLogs).catch(()=>{});
|
||||||
|
setShowLogs(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fmtDate = d => d ? new Date(d).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '–';
|
||||||
|
const fmtSize = b => b>1024*1024 ? (b/1024/1024).toFixed(1)+' MB' : (b/1024).toFixed(0)+' KB';
|
||||||
|
|
||||||
|
const DURATION_OPTS = [
|
||||||
|
[1,'1 Stunde'],[2,'2 Stunden'],[6,'6 Stunden'],[12,'12 Stunden'],
|
||||||
|
[24,'1 Tag'],[48,'2 Tage'],[72,'3 Tage'],[168,'1 Woche'],[336,'2 Wochen'],
|
||||||
|
];
|
||||||
|
const SIZE_OPTS = [1,2,5,10,20,50,100,250];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:12,flexWrap:'wrap',gap:8}}>
|
||||||
|
<div>
|
||||||
|
<div style={{color:'#fff',fontFamily:'monospace',fontSize:14,fontWeight:700}}>
|
||||||
|
📤 Upload-Ordner
|
||||||
|
</div>
|
||||||
|
{folder && <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||||||
|
Dateien landen in: 📁 {folder.name}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<div style={{display:'flex',gap:6}}>
|
||||||
|
<button onClick={loadLogs} style={{...S.btn('rgba(255,255,255,0.3)',true),fontSize:11,padding:'6px 12px'}}>
|
||||||
|
📋 Log
|
||||||
|
</button>
|
||||||
|
<button onClick={()=>setShowCreate(p=>!p)} style={{...S.btn('#4ecdc4'),padding:'7px 14px',fontSize:12}}>
|
||||||
|
{showCreate?'Abbrechen':'+ Link erstellen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Erstellen-Formular */}
|
||||||
|
{showCreate && (
|
||||||
|
<div style={{...S.card,marginBottom:12}}>
|
||||||
|
<div style={{...S.head,marginBottom:12}}>UPLOAD-LINK KONFIGURIEREN</div>
|
||||||
|
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,marginBottom:8}}>
|
||||||
|
<div>
|
||||||
|
<div style={{...S.head,fontSize:9,marginBottom:4}}>GÜLTIGKEITSDAUER</div>
|
||||||
|
<select value={form.expires_hours} onChange={e=>setForm(p=>({...p,expires_hours:Number(e.target.value)}))}
|
||||||
|
style={{...S.inp,width:'100%',cursor:'pointer'}}>
|
||||||
|
{DURATION_OPTS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{...S.head,fontSize:9,marginBottom:4}}>MAX. DATEIGRÖSSE</div>
|
||||||
|
<select value={form.max_size_mb} onChange={e=>setForm(p=>({...p,max_size_mb:Number(e.target.value)}))}
|
||||||
|
style={{...S.inp,width:'100%',cursor:'pointer'}}>
|
||||||
|
{SIZE_OPTS.map(v=><option key={v} value={v}>{v} MB</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{marginBottom:12}}>
|
||||||
|
<div style={{...S.head,fontSize:9,marginBottom:4}}>PASSWORT</div>
|
||||||
|
<div style={{display:'flex',gap:6}}>
|
||||||
|
<input value={form.password} onChange={e=>setForm(p=>({...p,password:e.target.value}))}
|
||||||
|
placeholder="Passwort vergeben" style={{...S.inp,flex:1,fontFamily:'monospace'}}/>
|
||||||
|
<button onClick={genPw} style={{...S.btn('#ffe66d',true),fontSize:11,padding:'0 12px',whiteSpace:'nowrap'}}>
|
||||||
|
⚡ Generieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{form.password && <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,marginTop:4}}>
|
||||||
|
Passwort: <strong style={{color:'#ffe66d'}}>{form.password}</strong> – notiere es jetzt
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<button onClick={create} disabled={busy} style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'10px 0',opacity:busy?0.5:1}}>
|
||||||
|
{busy?'…':'🔗 Link erstellen & kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Aktive Links */}
|
||||||
|
{shares.length===0 && !showCreate && (
|
||||||
|
<div style={{...S.card,textAlign:'center',padding:32}}>
|
||||||
|
<div style={{fontSize:28,marginBottom:8}}>📤</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}>
|
||||||
|
Noch kein Upload-Link. Erstelle einen Link um anderen das Hochladen zu erlauben.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shares.map(s => {
|
||||||
|
const expired = new Date(s.expires_at) < new Date();
|
||||||
|
return (
|
||||||
|
<div key={s.id} style={{...S.card,marginBottom:8,opacity:(!s.is_active||expired)?0.5:1}}>
|
||||||
|
<div style={{display:'flex',alignItems:'center',gap:10,flexWrap:'wrap'}}>
|
||||||
|
<div style={{flex:1,minWidth:0}}>
|
||||||
|
<div style={{display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
|
||||||
|
<span style={{color:!s.is_active||expired?'#ff6b9d':expired?'#ffe66d':'#4ecdc4',fontFamily:'monospace',fontSize:11,
|
||||||
|
border:`1px solid currentColor`,borderRadius:4,padding:'1px 6px',flexShrink:0}}>
|
||||||
|
{!s.is_active?'Deaktiviert':expired?'Abgelaufen':'Aktiv'}
|
||||||
|
</span>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
|
||||||
|
bis {fmtDate(s.expires_at)} · max {s.max_size_mb} MB · {s.upload_count||0} Upload(s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10,marginTop:3,
|
||||||
|
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
|
||||||
|
/u/{s.token}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{display:'flex',gap:5,flexShrink:0}}>
|
||||||
|
{s.is_active && !expired && (
|
||||||
|
<button onClick={()=>copyLink(s.token)}
|
||||||
|
style={{...S.btn(copied===s.token?'#4ecdc4':'#ffe66d',copied!==s.token),fontSize:10,padding:'4px 10px'}}>
|
||||||
|
{copied===s.token?'✓ Kopiert':'📋 Link'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{s.is_active && (
|
||||||
|
<button onClick={()=>deactivate(s.id)} style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'4px 10px'}}>✕</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Log-Ansicht */}
|
||||||
|
{showLogs && (
|
||||||
|
<div style={{...S.card,marginTop:12}}>
|
||||||
|
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
|
||||||
|
<div style={{...S.head,marginBottom:0}}>UPLOAD-PROTOKOLL</div>
|
||||||
|
<button onClick={()=>setShowLogs(false)} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:16}}>✕</button>
|
||||||
|
</div>
|
||||||
|
{logs.length===0
|
||||||
|
? <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11}}>Noch keine Uploads über Freigabe-Links.</div>
|
||||||
|
: logs.map(l=>(
|
||||||
|
<div key={l.id} style={{display:'flex',gap:10,padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
|
||||||
|
<span style={{fontSize:14,flexShrink:0}}>📄</span>
|
||||||
|
<div style={{flex:1,minWidth:0}}>
|
||||||
|
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{l.originalname}</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>{fmtDate(l.uploaded_at)} · {fmtSize(l.size)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dateien({ toast, mobile }) {
|
export default function Dateien({ toast, mobile }) {
|
||||||
const [data, setData] = useState({own:[],shared:[],sharedByMe:[]});
|
const [data, setData] = useState({own:[],shared:[],sharedByMe:[]});
|
||||||
const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]});
|
const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]});
|
||||||
@@ -329,6 +515,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
['own','Dateien', null],
|
['own','Dateien', null],
|
||||||
['shared','Geteilt mit mir', sharedCnt||null],
|
['shared','Geteilt mit mir', sharedCnt||null],
|
||||||
['sharedByMe','Geteilt von mir', byMeCnt||null],
|
['sharedByMe','Geteilt von mir', byMeCnt||null],
|
||||||
|
['upload','📤 Upload-Ordner', null],
|
||||||
].map(([k,l,cnt])=>(
|
].map(([k,l,cnt])=>(
|
||||||
<button key={k} onClick={()=>{setTab(k);setSearch('');setCurrentFolderId(null);setFolderPath([]);}} style={{
|
<button key={k} onClick={()=>{setTab(k);setSearch('');setCurrentFolderId(null);setFolderPath([]);}} style={{
|
||||||
padding:'6px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
padding:'6px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||||||
@@ -504,11 +691,14 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
<div style={{...S.card,textAlign:'center',padding:40}}>
|
<div style={{...S.card,textAlign:'center',padding:40}}>
|
||||||
<div style={{fontSize:28,marginBottom:10}}>📂</div>
|
<div style={{fontSize:28,marginBottom:10}}>📂</div>
|
||||||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}>
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}>
|
||||||
{search?'Keine Treffer.':tab==='own'?'Noch leer – Datei hochladen oder Ordner anlegen.':'Noch nichts geteilt.'}
|
{search?'Keine Treffer.':tab==='own'?'Noch leer – Datei hochladen oder Ordner anlegen.':tab==='upload'?'':' Noch nichts geteilt.'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user