Fix Kurz-URL: Shortening server-seitig beim Erstellen, kein CORS, sync kopieren
This commit is contained in:
@@ -521,13 +521,11 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='cal
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload_shares Fehlversuche-Tracking
|
// short_url auf upload_shares
|
||||||
{
|
{
|
||||||
const usCols = db.pragma('table_info(upload_shares)').map(c => c.name);
|
const cols = db.pragma('table_info(upload_shares)').map(c => c.name);
|
||||||
if (!usCols.includes('failed_attempts'))
|
if (!cols.includes('short_url'))
|
||||||
db.exec('ALTER TABLE upload_shares ADD COLUMN failed_attempts INTEGER DEFAULT 0');
|
db.exec('ALTER TABLE upload_shares ADD COLUMN short_url TEXT DEFAULT NULL');
|
||||||
if (!usCols.includes('deactivated_reason'))
|
|
||||||
db.exec("ALTER TABLE upload_shares ADD COLUMN deactivated_reason TEXT DEFAULT NULL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const https = require('https');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
@@ -8,6 +9,20 @@ const db = require('../../db');
|
|||||||
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
const { authenticate, requireAdmin } = require('../../middleware/auth');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Node-seitige URL-Kürzung (kein CORS-Problem)
|
||||||
|
function shortenUrl(longUrl) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const url = `https://is.gd/create.php?format=simple&url=${encodeURIComponent(longUrl)}`;
|
||||||
|
const req = https.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', d => data += d);
|
||||||
|
res.on('end', () => resolve(data.trim().startsWith('http') ? data.trim() : null));
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.setTimeout(5000, () => { req.destroy(); resolve(null); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
|
||||||
|
|
||||||
function generateToken() { return crypto.randomBytes(20).toString('base64url'); }
|
function generateToken() { return crypto.randomBytes(20).toString('base64url'); }
|
||||||
@@ -40,7 +55,7 @@ router.get('/', authenticate, (req, res) => {
|
|||||||
res.json({ shares, folder });
|
res.json({ shares, folder });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/', authenticate, (req, res) => {
|
router.post('/', authenticate, async (req, res) => {
|
||||||
if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error:'Keine Berechtigung' });
|
if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error:'Keine Berechtigung' });
|
||||||
const { password, expires_hours=24, max_size_mb=10 } = req.body;
|
const { password, expires_hours=24, max_size_mb=10 } = req.body;
|
||||||
if (!password?.trim()) return res.status(400).json({ error:'Passwort erforderlich' });
|
if (!password?.trim()) return res.status(400).json({ error:'Passwort erforderlich' });
|
||||||
@@ -53,7 +68,13 @@ router.post('/', authenticate, (req, res) => {
|
|||||||
VALUES (?,?,?,?,?,?,datetime('now','localtime'))
|
VALUES (?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
`).run(req.user.id, token, hash, expires, Number(max_size_mb), folder.id);
|
`).run(req.user.id, token, hash, expires, Number(max_size_mb), folder.id);
|
||||||
const share = db.prepare('SELECT * FROM upload_shares WHERE id=?').get(r.lastInsertRowid);
|
const share = db.prepare('SELECT * FROM upload_shares WHERE id=?').get(r.lastInsertRowid);
|
||||||
res.json({ ...share, url:`/u/${token}` });
|
// URL direkt beim Erstellen kürzen (server-seitig, kein CORS)
|
||||||
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
const fullUrl = `${baseUrl}/u/${token}`;
|
||||||
|
let shortUrl = null;
|
||||||
|
try { shortUrl = await shortenUrl(fullUrl); } catch {}
|
||||||
|
if (shortUrl) db.prepare('UPDATE upload_shares SET short_url=? WHERE id=?').run(shortUrl, share.id);
|
||||||
|
res.json({ ...share, short_url: shortUrl, url: fullUrl });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/:id', authenticate, (req, res) => {
|
router.delete('/:id', authenticate, (req, res) => {
|
||||||
|
|||||||
@@ -170,8 +170,6 @@ function UploadShareManager({ toast }) {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [copied, setCopied] = useState(null);
|
const [copied, setCopied] = useState(null);
|
||||||
const [sharePws, setSharePws] = useState({}); // {shareId: password} für Passwort-Kopieren
|
const [sharePws, setSharePws] = useState({}); // {shareId: password} für Passwort-Kopieren
|
||||||
const [shortUrls, setShortUrls] = useState({}); // {shareId: shortUrl}
|
|
||||||
const [shortening, setShortening] = useState(null); // shareId being shortened
|
|
||||||
|
|
||||||
const load = () => api('/upload-shares').then(d=>{ setShares(d.shares||[]); setFolder(d.folder); }).catch(()=>{});
|
const load = () => api('/upload-shares').then(d=>{ setShares(d.shares||[]); setFolder(d.folder); }).catch(()=>{});
|
||||||
useEffect(()=>{ load(); },[]);
|
useEffect(()=>{ load(); },[]);
|
||||||
@@ -220,41 +218,22 @@ function UploadShareManager({ toast }) {
|
|||||||
setShortening(null);
|
setShortening(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const doCopy = (text) => {
|
const copyLink = (shareId, token, shortUrl) => {
|
||||||
// Fallback-Kopieren ohne navigator.clipboard (funktioniert auch in PWA/mobile)
|
// Kurz-URL vom Server nehmen (bereits beim Erstellen generiert), sonst Original
|
||||||
|
const url = shortUrl || `${window.location.origin}/u/${token}`;
|
||||||
|
// Synchron kopieren – kein await nötig
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(text).catch(() => {
|
navigator.clipboard.writeText(url).catch(() => fallbackCopy(url));
|
||||||
const ta = document.createElement('textarea');
|
} else { fallbackCopy(url); }
|
||||||
ta.value = text; ta.style.cssText='position:fixed;opacity:0';
|
setCopied(token); setTimeout(()=>setCopied(null), 2500);
|
||||||
document.body.appendChild(ta); ta.focus(); ta.select();
|
|
||||||
document.execCommand('copy'); document.body.removeChild(ta);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const ta = document.createElement('textarea');
|
|
||||||
ta.value = text; ta.style.cssText='position:fixed;opacity:0';
|
|
||||||
document.body.appendChild(ta); ta.focus(); ta.select();
|
|
||||||
document.execCommand('copy'); document.body.removeChild(ta);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLink = (shareId, token) => {
|
const fallbackCopy = (text) => {
|
||||||
// SOFORT kopieren (synchron, innerhalb User-Gesture-Kontext)
|
const ta = document.createElement('textarea');
|
||||||
const url = shortUrls[shareId] || `${window.location.origin}/u/${token}`;
|
ta.value = text; ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0.01';
|
||||||
doCopy(url);
|
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||||
setCopied(token); setTimeout(()=>setCopied(null), 2500);
|
try { document.execCommand('copy'); } catch {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
// Falls noch keine Kurz-URL: im Hintergrund generieren (für nächsten Klick)
|
|
||||||
if (!shortUrls[shareId] && shortening !== shareId) {
|
|
||||||
setShortening(shareId);
|
|
||||||
const fullUrl = `${window.location.origin}/u/${token}`;
|
|
||||||
fetch(`https://is.gd/create.php?format=simple&url=${encodeURIComponent(fullUrl)}`)
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(short => {
|
|
||||||
if (short.startsWith('http')) setShortUrls(p=>({...p,[shareId]:short.trim()}));
|
|
||||||
})
|
|
||||||
.catch(()=>{})
|
|
||||||
.finally(()=>setShortening(null));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLogs = () => {
|
const loadLogs = () => {
|
||||||
@@ -366,9 +345,9 @@ function UploadShareManager({ toast }) {
|
|||||||
<div style={{display:'flex',gap:5,flexShrink:0}}>
|
<div style={{display:'flex',gap:5,flexShrink:0}}>
|
||||||
{!!s.is_active && !expired && (
|
{!!s.is_active && !expired && (
|
||||||
<>
|
<>
|
||||||
<button onClick={()=>copyLink(s.id,s.token)}
|
<button onClick={()=>copyLink(s.id, s.token, s.short_url)}
|
||||||
style={{...S.btn(copied===s.token?'#4ecdc4':'#ffe66d',copied!==s.token),fontSize:10,padding:'4px 10px'}}>
|
style={{...S.btn(copied===s.token?'#4ecdc4':'#ffe66d',copied!==s.token),fontSize:10,padding:'4px 10px'}}>
|
||||||
{shortening===s.id?'…':copied===s.token?'✓ Kopiert':'📋 Link'}
|
{copied===s.token?'✓ Kopiert':'📋 Link'}
|
||||||
</button>
|
</button>
|
||||||
{sharePws[s.id] && (
|
{sharePws[s.id] && (
|
||||||
<button onClick={()=>{ navigator.clipboard?.writeText(sharePws[s.id]).then(()=>toast('Passwort kopiert ✓')).catch(()=>toast(sharePws[s.id])); }}
|
<button onClick={()=>{ navigator.clipboard?.writeText(sharePws[s.id]).then(()=>toast('Passwort kopiert ✓')).catch(()=>toast(sharePws[s.id])); }}
|
||||||
|
|||||||
Reference in New Issue
Block a user