fix: media – requireAdmin korrekt, Header, Token-Hinweis

This commit is contained in:
2026-06-06 16:28:29 +02:00
parent 9969079bdd
commit 5717e67b6d
2 changed files with 51 additions and 58 deletions

View File

@@ -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());

View File

@@ -13,6 +13,10 @@ export default function Media() {
return ( return (
<div style={{ padding: '0 0 40px' }}> <div style={{ padding: '0 0 40px' }}>
<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' }}> <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={{
@@ -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
@@ -46,9 +50,7 @@ function MovieCard({ movie, favIds, onToggleFav }) {
<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}
@@ -56,7 +58,6 @@ function MovieCard({ movie, favIds, onToggleFav }) {
: <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,18 +68,14 @@ 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>
{/* Info */}
<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',
@@ -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());
@@ -137,7 +134,14 @@ 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 (
@@ -153,23 +157,17 @@ 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' });
@@ -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)',