fix: media – requireAdmin korrekt, Header, Token-Hinweis
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const db = require('../../db');
|
const db = require('../../db');
|
||||||
|
const { requireAdmin } = require('../../middleware/auth');
|
||||||
|
|
||||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||||
|
|
||||||
@@ -71,16 +72,14 @@ router.delete('/favorites/:tmdb_id', (req, res) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/tools/media/settings (admin only – gibt Token maskiert zurück)
|
// GET /api/tools/media/settings (admin only)
|
||||||
router.get('/settings', (req, res) => {
|
router.get('/settings', requireAdmin, (req, res) => {
|
||||||
if (!req.user.is_admin) return res.status(403).json({ error: 'Nur Admin' });
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
res.json({ configured: !!token });
|
res.json({ configured: !!token });
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /api/tools/media/settings (admin only)
|
// PUT /api/tools/media/settings (admin only)
|
||||||
router.put('/settings', (req, res) => {
|
router.put('/settings', requireAdmin, (req, res) => {
|
||||||
if (!req.user.is_admin) return res.status(403).json({ error: 'Nur Admin' });
|
|
||||||
const { tmdb_token } = req.body;
|
const { tmdb_token } = req.body;
|
||||||
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
|
if (typeof tmdb_token !== 'string') return res.status(400).json({ error: 'tmdb_token fehlt' });
|
||||||
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run('tmdb_token', tmdb_token.trim());
|
db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run('tmdb_token', tmdb_token.trim());
|
||||||
|
|||||||
@@ -13,15 +13,19 @@ export default function Media() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 0 40px' }}>
|
<div style={{ padding: '0 0 40px' }}>
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 24, flexWrap: 'wrap' }}>
|
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:20, margin:'0 0 20px' }}>
|
||||||
|
🎬 Media
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div style={{ display:'flex', gap:8, marginBottom:24, flexWrap:'wrap' }}>
|
||||||
{TABS.map(t => (
|
{TABS.map(t => (
|
||||||
<button key={t.id} onClick={() => setTab(t.id)} style={{
|
<button key={t.id} onClick={() => setTab(t.id)} style={{
|
||||||
padding: '7px 16px', borderRadius: 8, cursor: 'pointer',
|
padding:'7px 16px', borderRadius:8, cursor:'pointer',
|
||||||
border: tab === t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.12)',
|
border: tab===t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.12)',
|
||||||
background: tab === t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
background: tab===t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
||||||
color: tab === t.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
|
color: tab===t.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
|
||||||
fontFamily: "'Space Mono',monospace", fontSize: 13,
|
fontFamily:"'Space Mono',monospace", fontSize:13,
|
||||||
fontWeight: tab === t.id ? 700 : 400, transition: 'all 0.15s',
|
fontWeight: tab===t.id ? 700 : 400, transition:'all 0.15s',
|
||||||
}}>
|
}}>
|
||||||
{t.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -35,7 +39,7 @@ export default function Media() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Gemeinsame Film-Karte ─────────────────────────────────────────────────────
|
// ── Film-Karte ────────────────────────────────────────────────────────────────
|
||||||
function MovieCard({ movie, favIds, onToggleFav }) {
|
function MovieCard({ movie, favIds, onToggleFav }) {
|
||||||
const isFav = favIds.has(movie.id);
|
const isFav = favIds.has(movie.id);
|
||||||
const releaseDE = movie.release_date
|
const releaseDE = movie.release_date
|
||||||
@@ -44,19 +48,16 @@ function MovieCard({ movie, favIds, onToggleFav }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)',
|
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
||||||
borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
borderRadius:12, overflow:'hidden', display:'flex', flexDirection:'column',
|
||||||
transition: 'border-color 0.15s',
|
|
||||||
}}>
|
}}>
|
||||||
{/* Poster */}
|
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
||||||
<div style={{ position: 'relative', aspectRatio: '2/3', background: '#111', overflow: 'hidden' }}>
|
|
||||||
{movie.poster_path
|
{movie.poster_path
|
||||||
? <img src={POSTER + movie.poster_path} alt={movie.title}
|
? <img src={POSTER + movie.poster_path} alt={movie.title}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||||
: <div style={{ width:'100%',height:'100%',display:'flex',alignItems:'center',
|
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
||||||
justifyContent:'center',fontSize:40,color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
justifyContent:'center', fontSize:40, color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
||||||
}
|
}
|
||||||
{/* Rating Badge */}
|
|
||||||
{movie.vote_average > 0 && (
|
{movie.vote_average > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position:'absolute', top:8, left:8,
|
position:'absolute', top:8, left:8,
|
||||||
@@ -67,19 +68,15 @@ function MovieCard({ movie, favIds, onToggleFav }) {
|
|||||||
★ {movie.vote_average.toFixed(1)}
|
★ {movie.vote_average.toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Fav Button */}
|
|
||||||
<button onClick={() => onToggleFav(movie, isFav)} style={{
|
<button onClick={() => onToggleFav(movie, isFav)} style={{
|
||||||
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
|
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
|
||||||
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
|
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
|
||||||
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
|
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
transition:'transform 0.1s',
|
|
||||||
}}>
|
}}>
|
||||||
{isFav ? '❤' : '🤍'}
|
{isFav ? '❤' : '🤍'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
|
||||||
{/* Info */}
|
|
||||||
<div style={{ padding: '10px 12px', flex: 1, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
||||||
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
|
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
|
||||||
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
||||||
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
|
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
|
||||||
@@ -95,7 +92,7 @@ function MovieCard({ movie, favIds, onToggleFav }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Grid-Wrapper (now-playing / upcoming) ─────────────────────────────────────
|
// ── Grid (now-playing / upcoming) ─────────────────────────────────────────────
|
||||||
function MovieGrid({ endpoint, emptyText }) {
|
function MovieGrid({ endpoint, emptyText }) {
|
||||||
const [movies, setMovies] = useState([]);
|
const [movies, setMovies] = useState([]);
|
||||||
const [favIds, setFavIds] = useState(new Set());
|
const [favIds, setFavIds] = useState(new Set());
|
||||||
@@ -120,7 +117,7 @@ function MovieGrid({ endpoint, emptyText }) {
|
|||||||
const toggleFav = async (movie, isFav) => {
|
const toggleFav = async (movie, isFav) => {
|
||||||
try {
|
try {
|
||||||
if (isFav) {
|
if (isFav) {
|
||||||
await api(`/tools/media/favorites/${movie.id}`, { method: 'DELETE' });
|
await api(`/tools/media/favorites/${movie.id}`, { method:'DELETE' });
|
||||||
setFavIds(prev => { const s = new Set(prev); s.delete(movie.id); return s; });
|
setFavIds(prev => { const s = new Set(prev); s.delete(movie.id); return s; });
|
||||||
} else {
|
} else {
|
||||||
await api('/tools/media/favorites', { body: {
|
await api('/tools/media/favorites', { body: {
|
||||||
@@ -137,14 +134,21 @@ function MovieGrid({ endpoint, emptyText }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
|
||||||
if (error) return <StatusBox color="#ff6b9d">⚠ {error}</StatusBox>;
|
if (error) return <StatusBox color="#ff6b9d">
|
||||||
|
⚠ {error}
|
||||||
|
{error.includes('Token') && (
|
||||||
|
<div style={{ marginTop:8, fontSize:12, color:'rgba(255,255,255,0.4)' }}>
|
||||||
|
→ Einstellungen → Benutzer → <span style={{ color:'#4ecdc4' }}>TMDB API (KINO-FEATURE)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StatusBox>;
|
||||||
if (!movies.length) return <StatusBox>{emptyText}</StatusBox>;
|
if (!movies.length) return <StatusBox>{emptyText}</StatusBox>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display:'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
gridTemplateColumns:'repeat(auto-fill, minmax(140px, 1fr))',
|
||||||
gap: 14,
|
gap:14,
|
||||||
}}>
|
}}>
|
||||||
{movies.map(m => (
|
{movies.map(m => (
|
||||||
<MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={toggleFav} />
|
<MovieCard key={m.id} movie={m} favIds={favIds} onToggleFav={toggleFav} />
|
||||||
@@ -153,26 +157,20 @@ function MovieGrid({ endpoint, emptyText }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Favoriten-Tab ─────────────────────────────────────────────────────────────
|
// ── Favoriten ─────────────────────────────────────────────────────────────────
|
||||||
function FavoritenTab() {
|
function FavoritenTab() {
|
||||||
const [favs, setFavs] = useState([]);
|
const [favs, setFavs] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
useEffect(() => {
|
||||||
setLoading(true); setError('');
|
setLoading(true); setError('');
|
||||||
try {
|
api('/tools/media/favorites').then(setFavs).catch(e => setError(e.message)).finally(() => setLoading(false));
|
||||||
const data = await api('/tools/media/favorites');
|
|
||||||
setFavs(data);
|
|
||||||
} catch (e) { setError(e.message); }
|
|
||||||
finally { setLoading(false); }
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
|
||||||
|
|
||||||
const removeFav = async (tmdbId) => {
|
const removeFav = async (tmdbId) => {
|
||||||
try {
|
try {
|
||||||
await api(`/tools/media/favorites/${tmdbId}`, { method: 'DELETE' });
|
await api(`/tools/media/favorites/${tmdbId}`, { method:'DELETE' });
|
||||||
setFavs(prev => prev.filter(f => f.tmdb_id !== tmdbId));
|
setFavs(prev => prev.filter(f => f.tmdb_id !== tmdbId));
|
||||||
} catch (e) { alert(e.message); }
|
} catch (e) { alert(e.message); }
|
||||||
};
|
};
|
||||||
@@ -182,11 +180,7 @@ function FavoritenTab() {
|
|||||||
if (!favs.length) return <StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>;
|
if (!favs.length) return <StatusBox>Noch keine Favoriten – markiere Filme mit ❤</StatusBox>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(140px, 1fr))', gap:14 }}>
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
||||||
gap: 14,
|
|
||||||
}}>
|
|
||||||
{favs.map(f => (
|
{favs.map(f => (
|
||||||
<div key={f.tmdb_id} style={{
|
<div key={f.tmdb_id} style={{
|
||||||
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.08)',
|
||||||
@@ -195,24 +189,24 @@ function FavoritenTab() {
|
|||||||
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
<div style={{ position:'relative', aspectRatio:'2/3', background:'#111', overflow:'hidden' }}>
|
||||||
{f.poster_path
|
{f.poster_path
|
||||||
? <img src={POSTER + f.poster_path} alt={f.title}
|
? <img src={POSTER + f.poster_path} alt={f.title}
|
||||||
style={{ width:'100%',height:'100%',objectFit:'cover',display:'block' }} />
|
style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
|
||||||
: <div style={{ width:'100%',height:'100%',display:'flex',alignItems:'center',
|
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
|
||||||
justifyContent:'center',fontSize:40,color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
justifyContent:'center', fontSize:40, color:'rgba(255,255,255,0.15)' }}>🎬</div>
|
||||||
}
|
}
|
||||||
<button onClick={() => removeFav(f.tmdb_id)} style={{
|
<button onClick={() => removeFav(f.tmdb_id)} style={{
|
||||||
position:'absolute',top:6,right:6,background:'rgba(0,0,0,0.6)',
|
position:'absolute', top:6, right:6, background:'rgba(0,0,0,0.6)',
|
||||||
border:'none',borderRadius:'50%',width:32,height:32,cursor:'pointer',
|
border:'none', borderRadius:'50%', width:32, height:32, cursor:'pointer',
|
||||||
fontSize:16,display:'flex',alignItems:'center',justifyContent:'center',
|
fontSize:16, display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
}}>❤</button>
|
}}>❤</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
|
<div style={{ padding:'10px 12px', flex:1, display:'flex', flexDirection:'column', gap:4 }}>
|
||||||
<div style={{ fontSize:13,fontWeight:700,color:'#fff',lineHeight:1.3,
|
<div style={{ fontSize:13, fontWeight:700, color:'#fff', lineHeight:1.3,
|
||||||
fontFamily:"'Space Mono',monospace",overflow:'hidden',
|
fontFamily:"'Space Mono',monospace", overflow:'hidden',
|
||||||
display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical' }}>
|
display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>
|
||||||
{f.title}
|
{f.title}
|
||||||
</div>
|
</div>
|
||||||
{f.release_date_de && (
|
{f.release_date_de && (
|
||||||
<div style={{ fontSize:11,color:'rgba(255,255,255,0.4)',fontFamily:"'Space Mono',monospace" }}>
|
<div style={{ fontSize:11, color:'rgba(255,255,255,0.4)', fontFamily:"'Space Mono',monospace" }}>
|
||||||
🇩🇪 {f.release_date_de}
|
🇩🇪 {f.release_date_de}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user