362 lines
18 KiB
JavaScript
362 lines
18 KiB
JavaScript
const express = require('express');
|
||
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';
|
||
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||
|
||
const getSetting = key => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value;
|
||
|
||
const storage = multer.diskStorage({
|
||
destination: UPLOAD_DIR,
|
||
filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${path.extname(file.originalname)}`),
|
||
});
|
||
const getUpload = () => multer({
|
||
storage,
|
||
limits: { fileSize: parseInt(getSetting('file_max_size_mb') || '50') * 1024 * 1024 },
|
||
fileFilter: (req, file, cb) => {
|
||
const allowed = (getSetting('file_allowed_ext') || '').split(',').map(e => e.trim().toLowerCase()).filter(Boolean);
|
||
if (!allowed.length) return cb(null, true);
|
||
const ext = path.extname(file.originalname).toLowerCase();
|
||
allowed.includes(ext) ? cb(null, true) : cb(new Error(`Dateityp nicht erlaubt: ${ext}`));
|
||
},
|
||
});
|
||
|
||
// Walk folder tree upward to check if any ancestor is shared with user
|
||
function isInSharedFolder(folderId, uid) {
|
||
let cur = folderId;
|
||
while (cur) {
|
||
if (db.prepare('SELECT id FROM folder_shares WHERE folder_id=? AND shared_with=?').get(cur, uid)) return true;
|
||
const p = db.prepare('SELECT parent_id FROM folders WHERE id=?').get(cur);
|
||
cur = p?.parent_id || null;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const enrichFolder = (folder) => ({
|
||
...folder,
|
||
fileCount: db.prepare('SELECT COUNT(*) c FROM files WHERE folder_id=?').get(folder.id).c,
|
||
subCount: db.prepare('SELECT COUNT(*) c FROM folders WHERE parent_id=?').get(folder.id).c,
|
||
totalSize: db.prepare('SELECT COALESCE(SUM(size),0) s FROM files WHERE folder_id=?').get(folder.id).s,
|
||
});
|
||
|
||
// ── Ordner ────────────────────────────────────────────────────────────────────
|
||
router.get('/folders', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const parentId = req.query.parent_id ? parseInt(req.query.parent_id) : null;
|
||
|
||
// Own folders in this directory
|
||
const own = parentId
|
||
? db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id=? ORDER BY name').all(uid, parentId)
|
||
: db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id IS NULL ORDER BY name').all(uid);
|
||
|
||
// Shared folders
|
||
let shared = [];
|
||
if (!parentId) {
|
||
shared = db.prepare(`
|
||
SELECT f.*, u.username as owner,
|
||
GROUP_CONCAT(su.username) as shared_with_names,
|
||
MIN(fs.created_at) as shared_at
|
||
FROM folders f JOIN users u ON u.id=f.user_id
|
||
JOIN folder_shares fs ON fs.folder_id=f.id
|
||
JOIN users su ON su.id=fs.shared_with
|
||
WHERE fs.shared_with=?
|
||
GROUP BY f.id ORDER BY f.name
|
||
`).all(uid);
|
||
} else if (isInSharedFolder(parentId, uid)) {
|
||
const parentFolder = db.prepare('SELECT * FROM folders WHERE id=?').get(parentId);
|
||
if (parentFolder) {
|
||
shared = db.prepare('SELECT f.*, u.username as owner FROM folders f JOIN users u ON u.id=f.user_id WHERE f.parent_id=? AND f.user_id!=? ORDER BY f.name').all(parentId, uid);
|
||
}
|
||
}
|
||
|
||
// Folders shared BY me (root only)
|
||
const sharedByMe = parentId ? [] : db.prepare(`
|
||
SELECT DISTINCT f.*, u.username as owner,
|
||
GROUP_CONCAT(su.username) as shared_with_names,
|
||
MIN(fs.created_at) as shared_at
|
||
FROM folders f JOIN users u ON u.id=f.user_id
|
||
JOIN folder_shares fs ON fs.folder_id=f.id
|
||
JOIN users su ON su.id=fs.shared_with
|
||
WHERE f.user_id=?
|
||
GROUP BY f.id ORDER BY f.name
|
||
`).all(uid);
|
||
|
||
res.json({ own: own.map(enrichFolder), shared: shared.map(enrichFolder), sharedByMe: sharedByMe.map(enrichFolder) });
|
||
});
|
||
|
||
router.post('/folders', authenticate, (req, res) => {
|
||
const { name, parent_id } = req.body;
|
||
if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' });
|
||
const parentId = parent_id ? parseInt(parent_id) : null;
|
||
if (parentId) {
|
||
const parent = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(parentId, req.user.id);
|
||
if (!parent) return res.status(404).json({ error: 'Überordner nicht gefunden' });
|
||
}
|
||
const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(req.user.id, name.trim(), parentId);
|
||
res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid)));
|
||
});
|
||
|
||
router.delete('/folders/:id', authenticate, (req, res) => {
|
||
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
// Delete all files inside
|
||
const files = db.prepare('SELECT * FROM files WHERE folder_id=? AND user_id=?').all(folder.id, req.user.id);
|
||
for (const f of files) { try { fs.unlinkSync(path.join(UPLOAD_DIR, f.filename)); } catch {} }
|
||
db.prepare('DELETE FROM files WHERE folder_id=? AND user_id=?').run(folder.id, req.user.id);
|
||
db.prepare('DELETE FROM folders WHERE id=?').run(folder.id);
|
||
res.json({ success: true });
|
||
});
|
||
|
||
router.get('/folders/:id/shares', authenticate, (req, res) => {
|
||
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
res.json(db.prepare('SELECT u.id,u.username,fs.created_at as shared_at FROM folder_shares fs JOIN users u ON u.id=fs.shared_with WHERE fs.folder_id=?').all(folder.id));
|
||
});
|
||
|
||
router.post('/folders/:id/share', authenticate, (req, res) => {
|
||
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username);
|
||
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||
if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' });
|
||
db.prepare('INSERT OR IGNORE INTO folder_shares (folder_id,shared_by,shared_with) VALUES (?,?,?)').run(folder.id, req.user.id, target.id);
|
||
res.json({ success: true });
|
||
});
|
||
|
||
router.delete('/folders/:id/share/:userId', authenticate, (req, res) => {
|
||
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
db.prepare('DELETE FROM folder_shares WHERE folder_id=? AND shared_with=?').run(folder.id, req.params.userId);
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// ── Dateien ───────────────────────────────────────────────────────────────────
|
||
router.get('/', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const folderId = req.query.folder_id ? parseInt(req.query.folder_id) : null;
|
||
|
||
// Own files in this directory
|
||
const own = folderId
|
||
? db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id=? ORDER BY f.created_at DESC').all(uid, folderId)
|
||
: db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id IS NULL ORDER BY f.created_at DESC').all(uid);
|
||
|
||
// Files shared BY me
|
||
const sharedByMe = folderId ? [] : db.prepare(`
|
||
SELECT DISTINCT f.*, u.username as owner,
|
||
GROUP_CONCAT(su.username) as shared_with_names,
|
||
MIN(s.created_at) as shared_at,
|
||
MAX(s.accessed_at) as accessed_at,
|
||
MAX(CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END) as has_password
|
||
FROM files f JOIN users u ON u.id=f.user_id
|
||
JOIN file_shares s ON s.file_id=f.id
|
||
JOIN users su ON su.id=s.shared_with
|
||
WHERE f.user_id=?
|
||
GROUP BY f.id ORDER BY f.created_at DESC
|
||
`).all(uid);
|
||
|
||
// Files shared with me
|
||
let shared = [];
|
||
if (!folderId) {
|
||
shared = db.prepare(`
|
||
SELECT f.*, u.username as owner, sb.username as shared_by_name,
|
||
CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password
|
||
FROM files f JOIN users u ON u.id=f.user_id
|
||
JOIN file_shares s ON s.file_id=f.id
|
||
JOIN users sb ON sb.id=s.shared_by
|
||
WHERE s.shared_with=?
|
||
ORDER BY f.created_at DESC
|
||
`).all(uid);
|
||
} else if (isInSharedFolder(folderId, uid)) {
|
||
const folder = db.prepare('SELECT * FROM folders WHERE id=?').get(folderId);
|
||
if (folder) {
|
||
shared = db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.folder_id=? AND f.user_id=? ORDER BY f.created_at DESC').all(folderId, folder.user_id);
|
||
}
|
||
}
|
||
|
||
res.json({ own, shared, sharedByMe });
|
||
});
|
||
|
||
router.post('/', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50');
|
||
const totalLimitMb= parseInt(getSetting('file_total_size_mb') || '500');
|
||
const usedBytes = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
|
||
if (usedBytes >= totalLimitMb * 1024 * 1024)
|
||
return res.status(400).json({ error: `Gesamtlimit von ${totalLimitMb} MB erreicht` });
|
||
getUpload().single('file')(req, res, err => {
|
||
if (err) return res.status(400).json({ error: err.message });
|
||
if (!req.file) return res.status(400).json({ error: 'Keine Datei' });
|
||
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
|
||
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
|
||
.run(uid, req.file.filename, req.file.originalname, req.file.mimetype||'application/octet-stream', req.file.size, folderId);
|
||
res.json(db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid));
|
||
});
|
||
});
|
||
|
||
// ── 3D-Modelle Hilfsendpoints ─────────────────────────────────────────────────
|
||
|
||
// Sucht einen Ordner ohne ihn anzulegen – gibt null zurück wenn nicht gefunden
|
||
router.get('/find-folder', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const { name, parent_id } = req.query;
|
||
if (!name?.trim()) return res.json(null);
|
||
const parentId = parent_id ? parseInt(parent_id) : null;
|
||
const folder = parentId
|
||
? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId)
|
||
: db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim());
|
||
res.json(folder ? enrichFolder(folder) : null);
|
||
});
|
||
|
||
// Stellt sicher dass ein Ordner existiert (Name + optionaler parent), gibt ihn zurück
|
||
router.post('/ensure-folder', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const { name, parent_id } = req.body;
|
||
if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' });
|
||
const parentId = parent_id ? parseInt(parent_id) : null;
|
||
|
||
// Suche existierenden Ordner
|
||
const existing = parentId
|
||
? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId)
|
||
: db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim());
|
||
|
||
if (existing) return res.json(enrichFolder(existing));
|
||
|
||
const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(uid, name.trim(), parentId);
|
||
res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid)));
|
||
});
|
||
|
||
// Upload 3D-Dateien (ohne Ext-Beschränkung, für den Kalkulator)
|
||
const upload3d = multer({
|
||
storage,
|
||
limits: { fileSize: parseInt(process.env.MAX_3D_SIZE_MB || '200') * 1024 * 1024 },
|
||
});
|
||
|
||
router.post('/upload-3d', authenticate, (req, res) => {
|
||
upload3d.array('files', 20)(req, res, err => {
|
||
if (err) return res.status(400).json({ error: err.message });
|
||
if (!req.files?.length) return res.status(400).json({ error: 'Keine Dateien' });
|
||
const uid = req.user.id;
|
||
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
|
||
const inserted = [];
|
||
for (const f of req.files) {
|
||
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
|
||
.run(uid, f.filename, f.originalname, f.mimetype || 'application/octet-stream', f.size, folderId);
|
||
inserted.push(r.lastInsertRowid);
|
||
}
|
||
res.json({ uploaded: inserted.length });
|
||
});
|
||
});
|
||
|
||
router.get('/storage', authenticate, (req, res) => {
|
||
const uid = req.user.id;
|
||
const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
|
||
const count = db.prepare('SELECT COUNT(*) AS c FROM files WHERE user_id=?').get(uid).c;
|
||
const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50');
|
||
const totalLimitMb = parseInt(getSetting('file_total_size_mb') || '500');
|
||
res.json({ used, count, maxSizeMb, totalLimitMb });
|
||
});
|
||
|
||
router.get('/:id/download', authenticate, async (req, res) => {
|
||
const uid = req.user.id;
|
||
const file = db.prepare('SELECT * FROM files WHERE id=?').get(req.params.id);
|
||
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
|
||
const isOwner = file.user_id === uid;
|
||
const share = db.prepare('SELECT * FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, uid);
|
||
const inSharedDir = file.folder_id && isInSharedFolder(file.folder_id, uid);
|
||
|
||
if (!isOwner && !share && !inSharedDir) return res.status(403).json({ error: 'Kein Zugriff' });
|
||
|
||
// Passwortprüfung für Shares (Owner braucht kein Passwort)
|
||
if (!isOwner && share?.password_hash) {
|
||
const bcrypt = require('bcryptjs');
|
||
const pw = req.query.password || req.headers['x-share-password'] || '';
|
||
const ok = pw && await bcrypt.compare(pw, share.password_hash);
|
||
if (!ok) return res.status(403).json({
|
||
error: 'Falsches Passwort',
|
||
passwordRequired: !pw // true = noch kein PW eingegeben, false = falsches PW
|
||
});
|
||
}
|
||
|
||
const fp = path.join(UPLOAD_DIR, file.filename);
|
||
if (!fs.existsSync(fp)) return res.status(404).json({ error: 'Datei fehlt' });
|
||
|
||
// Zugriff tracken (immer aktualisieren damit es sicher gesetzt wird)
|
||
if (share) {
|
||
db.prepare("UPDATE file_shares SET accessed_at = datetime('now','localtime') WHERE file_id=? AND shared_with=?")
|
||
.run(file.id, uid);
|
||
}
|
||
|
||
res.download(fp, file.originalname);
|
||
});
|
||
|
||
router.delete('/:id', authenticate, (req, res) => {
|
||
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
try { fs.unlinkSync(path.join(UPLOAD_DIR, file.filename)); } catch {}
|
||
db.prepare('DELETE FROM files WHERE id=?').run(file.id);
|
||
res.json({ success: true });
|
||
});
|
||
|
||
router.get('/:id/shares', authenticate, (req, res) => {
|
||
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
res.json(db.prepare(`
|
||
SELECT u.id, u.username, s.created_at as shared_at,
|
||
s.accessed_at,
|
||
CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password
|
||
FROM file_shares s JOIN users u ON u.id=s.shared_with
|
||
WHERE s.file_id=?
|
||
`).all(file.id));
|
||
});
|
||
|
||
router.post('/:id/share', authenticate, async (req, res) => {
|
||
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username);
|
||
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||
if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' });
|
||
let passwordHash = null;
|
||
if (req.body.password) {
|
||
const bcrypt = require('bcryptjs');
|
||
passwordHash = await bcrypt.hash(req.body.password, 10);
|
||
}
|
||
const existing = db.prepare('SELECT id FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, target.id);
|
||
if (existing) {
|
||
db.prepare('UPDATE file_shares SET password_hash=?, accessed_at=NULL WHERE file_id=? AND shared_with=?')
|
||
.run(passwordHash, file.id, target.id);
|
||
} else {
|
||
db.prepare('INSERT INTO file_shares (file_id,shared_by,shared_with,password_hash) VALUES (?,?,?,?)')
|
||
.run(file.id, req.user.id, target.id, passwordHash);
|
||
}
|
||
res.json({ success: true });
|
||
});
|
||
|
||
router.delete('/:id/share/:userId', authenticate, (req, res) => {
|
||
const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||
if (!file) return res.status(404).json({ error: 'Nicht gefunden' });
|
||
db.prepare('DELETE FROM file_shares WHERE file_id=? AND shared_with=?').run(file.id, req.params.userId);
|
||
res.json({ success: true });
|
||
});
|
||
|
||
// Settings
|
||
router.get('/settings', authenticate, requireAdmin, (req, res) => {
|
||
const s = db.prepare('SELECT * FROM admin_settings').all();
|
||
const obj = {}; for (const r of s) obj[r.key] = r.value;
|
||
res.json(obj);
|
||
});
|
||
router.put('/settings', authenticate, requireAdmin, (req, res) => {
|
||
for (const key of ['file_max_size_mb','file_total_size_mb','file_allowed_ext']) {
|
||
if (req.body[key] !== undefined)
|
||
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run(key, String(req.body[key]));
|
||
}
|
||
res.json({ success: true });
|
||
});
|
||
|
||
module.exports = router;
|