'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
/