Seafile sign-in follows the person across devices; sturdier Plex player; audiobooks frame with server-side secret sign-in
build-container / build (push) Successful in 3m16s
build-container / build (push) Successful in 3m16s
- Seafile token is saved on the server (encrypted) so Settings shows the real signed-in state everywhere - Player: whole-track cache for current and next two, stall recovery, instant track changes, shuffle/repeat, Plex play counts, transcode fallback, Plex request timeout - Audio tab: toolbar, full-height kept-alive frame; /api/audiobooks/enter adds the secret login link on the server Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a29d500d3
commit
bc3710807d
8 files changed
+263
-44
No files matched your search
@@ -10,6 +10,18 @@ const PORT = process.env.PORT || 8080;
|
||||
const SEAFILE_URL = (process.env.SEAFILE_URL || 'https://seafile.necloud.us').replace(/\/$/, '');
|
||||
const ONLYOFFICE_URL = (process.env.ONLYOFFICE_URL || 'https://onlyoffice.necloud.us').replace(/\/$/, '');
|
||||
const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.necloud.us').replace(/\/$/, '');
|
||||
const AUDIOBOOKS_SSO_URL = process.env.AUDIOBOOKS_SSO_URL || AUDIOBOOKS_URL + '/sso';
|
||||
const AUDIOBOOKS_SSO_SECRET = process.env.AUDIOBOOKS_SSO_SECRET || '';
|
||||
// Back-door sign-in for the audiobooks site. AUDIOBOOKS_LOGIN_LINK is the site's own secret link (the key is part of it); anyone who has it
|
||||
// is signed in there with full access, so it lives only in the server's environment: never sent to the page, never logged.
|
||||
// Alternatively AUDIOBOOKS_SSO_SECRET makes short-lived signed links per person.
|
||||
const AUDIOBOOKS_LOGIN_LINK = process.env.AUDIOBOOKS_LOGIN_LINK || '';
|
||||
function audiobooksLogin(user) {
|
||||
if (AUDIOBOOKS_LOGIN_LINK) return AUDIOBOOKS_LOGIN_LINK;
|
||||
if (!AUDIOBOOKS_SSO_SECRET) return '';
|
||||
const body = Buffer.from(JSON.stringify({ u: user.name, id: user.id, exp: Math.floor(Date.now() / 1000) + 120, n: crypto.randomBytes(8).toString('hex') })).toString('base64url');
|
||||
return AUDIOBOOKS_SSO_URL + '?token=' + body + '.' + crypto.createHmac('sha256', AUDIOBOOKS_SSO_SECRET).update(body).digest('base64url');
|
||||
}
|
||||
const auth = require('./auth');
|
||||
const cal = require('./cal');
|
||||
const tasks = require('./tasks');
|
||||
@@ -306,7 +318,9 @@ async function plexProxy(req, res) {
|
||||
if (!st.srv || !st.stk) { res.writeHead(401); return res.end(); }
|
||||
const ac = new AbortController();
|
||||
res.on('close', () => ac.abort());
|
||||
const attempt = s => fetch(s.srv + req.url.slice('/plex'.length), { signal: ac.signal,
|
||||
// A server that accepts the connection but never answers must not leave the player waiting forever: give up after 15 s without a response.
|
||||
const attempt = async s => { const hc = new AbortController(), t = setTimeout(() => hc.abort(), 15000); try { return await tryFetch(s, AbortSignal.any([ac.signal, hc.signal])); } finally { clearTimeout(t); } };
|
||||
const tryFetch = (s, signal) => fetch(s.srv + req.url.slice('/plex'.length), { signal,
|
||||
headers: { ...PLEX_HEADERS, 'X-Plex-Token': s.stk, Accept: req.headers.accept || 'application/json', 'Accept-Encoding': 'identity', ...(req.headers.range ? { range: req.headers.range } : {}) } });
|
||||
let up = null;
|
||||
try { up = await attempt(st); } catch { up = null; }
|
||||
@@ -381,6 +395,7 @@ const ACCESS = [
|
||||
[/^\/sf\//, ['files', 'office']],
|
||||
[/^\/api\/oo\/config$/, ['office', 'files']],
|
||||
[/^\/(api\/plex\/|plex\/)/, ['music']],
|
||||
[/^\/api\/audiobooks\//, ['audio']],
|
||||
[/^\/api\/cal\//, ['calendar']],
|
||||
[/^\/api\/tasks/, ['tasks']],
|
||||
[/^\/api\/(search|ddg)$/, ['search']],
|
||||
@@ -424,6 +439,10 @@ async function handleRequest(req, res) {
|
||||
if (pathname === '/api/search') return search.handle(req, res);
|
||||
if (req.url.startsWith('/api/tasks')) return tasks.handle(req, res);
|
||||
if (req.url.startsWith('/api/cal/')) return cal.handle(req, res);
|
||||
if (pathname === '/api/audiobooks/enter') { // the secret sign-in link is added here, on the server, and never appears in the app's page or script
|
||||
const to = audiobooksLogin(user);
|
||||
res.writeHead(302, { location: to || AUDIOBOOKS_URL, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' }); return res.end();
|
||||
}
|
||||
if (req.url.startsWith('/api/plex/')) return plexApi(req, res);
|
||||
if (req.url.startsWith('/plex/') && req.method === 'GET') return plexProxy(req, res);
|
||||
if (req.url.startsWith('/sf/')) return proxySeafile(req, res);
|
||||
|
||||
Reference in new issue
Block a user