'use strict'; const path = require('path'); const http = require('http'); const express = require('express'); const { WebSocketServer } = require('ws'); const { EMBY_SERVER_URL, SESSION_COOKIE, SESSION_TTL_MS, login, getSession, endSession, parseCookies, sessionFromRequest } = require('./embyAuth'); const { embyFetch, proxyEmbyResponse } = require('./embyProxy'); const { PartyManager } = require('./partyManager'); const { attachWebSocketServer } = require('./wsHandler'); const app = express(); app.set('trust proxy', 1); // behind openresty/nginx - needed for req.secure to reflect the real scheme app.use(express.json()); // Everything the browser needs - the app shell AND every Emby call - is // same-origin from here on. See docs/architecture.md for why: the previous // version had the browser call Emby directly and hit real CORS problems. app.use(express.static(path.join(__dirname, '..', '..', 'client'))); function requireSession(req, res, next) { const session = sessionFromRequest(req); if (!session) { res.status(401).json({ error: 'Session expirée, reconnecte-toi.' }); return; } req.embySession = session; next(); } // --- Auth ---------------------------------------------------------------- // The browser only ever sends a username/password once, to us. From then on // it holds an opaque session cookie - never Emby's own access token. app.post('/api/auth/login', async (req, res) => { const { username, password } = req.body || {}; if (!username || !password) { res.status(400).json({ error: "Nom d'utilisateur et mot de passe requis." }); return; } const sessionId = await login(username, password); if (!sessionId) { res.status(401).json({ error: 'Identifiants refusés.' }); return; } res.cookie(SESSION_COOKIE, sessionId, { httpOnly: true, sameSite: 'lax', secure: req.secure, maxAge: SESSION_TTL_MS }); res.json({ userName: getSession(sessionId).userName }); }); app.post('/api/auth/logout', (req, res) => { const cookies = parseCookies(req.headers.cookie); if (cookies[SESSION_COOKIE]) { endSession(cookies[SESSION_COOKIE]); } res.clearCookie(SESSION_COOKIE); res.json({ ok: true }); }); app.get('/api/auth/me', requireSession, (req, res) => { res.json({ userName: req.embySession.userName }); }); // --- Catalog (proxied - the browser never talks to Emby directly) -------- app.get('/api/catalog/search', requireSession, async (req, res) => { const q = (req.query.q || '').trim(); if (q.length < 2) { res.json({ items: [] }); return; } const params = new URLSearchParams({ searchTerm: q, IncludeItemTypes: 'Movie,Episode,Series', Recursive: 'true', Fields: 'Overview,ProductionYear', ImageTypeLimit: '1', Limit: '24' }); const embyRes = await embyFetch(`/Users/${req.embySession.userId}/Items?${params}`, req.embySession.embyToken); if (!embyRes.ok) { res.status(502).json({ error: 'Recherche impossible côté Emby.' }); return; } const data = await embyRes.json(); res.json({ items: (data.Items || []).map(toItemDto) }); }); app.get('/api/catalog/item/:itemId', requireSession, async (req, res) => { const embyRes = await embyFetch(`/Users/${req.embySession.userId}/Items/${req.params.itemId}`, req.embySession.embyToken); if (!embyRes.ok) { res.status(embyRes.status).json({ error: 'Titre introuvable.' }); return; } res.json(toItemDto(await embyRes.json())); }); // Images/video are requested by /