Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
177
dashcaddy-api/routes/auth/session-handlers.js
Normal file
177
dashcaddy-api/routes/auth/session-handlers.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
||||
const { createCache, CACHE_CONFIGS } = require('../../cache-config');
|
||||
|
||||
module.exports = function(ctx) {
|
||||
// App session cache for auto-login
|
||||
const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
|
||||
|
||||
async function getAppSession(serviceId, baseUrl, username, password) {
|
||||
const cached = appSessionCache.get(serviceId);
|
||||
if (cached && cached.exp > Date.now()) {
|
||||
if (cached.failed) return null;
|
||||
return cached.cookies;
|
||||
}
|
||||
|
||||
let loginUrl, loginBody, contentType = 'application/x-www-form-urlencoded';
|
||||
const extraHeaders = {};
|
||||
let expectJsonToken = false;
|
||||
const formEncode = (s) => encodeURIComponent(s).replace(/\*/g, '%2A');
|
||||
|
||||
switch (serviceId) {
|
||||
case 'torrent':
|
||||
loginUrl = `${baseUrl}api/v2/auth/login`;
|
||||
loginBody = `username=${formEncode(username)}&password=${formEncode(password)}`;
|
||||
extraHeaders['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
break;
|
||||
case 'router': {
|
||||
const routerBody = `username=${formEncode(username)}&password=${formEncode(password)}&Continue=Continue`;
|
||||
try {
|
||||
const { spawnSync } = require('child_process');
|
||||
const proc = spawnSync('wget', [
|
||||
'-q', '-S', `--post-data=${routerBody}`, '-O', '/dev/null',
|
||||
`${baseUrl}/cgi-bin/login.ha`
|
||||
], { timeout: 5000, encoding: 'utf8' });
|
||||
const result = (proc.stderr || '').split('\n').slice(0, 2).join('\n');
|
||||
const locationMatch = result.match(/Location:\s*(.+)/);
|
||||
const location = locationMatch ? locationMatch[1].trim() : '';
|
||||
if (location && !location.includes('login')) {
|
||||
appSessionCache.set(serviceId, { cookies: '__ip_session=1', exp: Date.now() + SESSION_TTL.IP_SESSION });
|
||||
ctx.log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId });
|
||||
return '__ip_session=1';
|
||||
}
|
||||
ctx.log.warn('auth', 'Router auto-login failed', { serviceId });
|
||||
} catch (e) {
|
||||
ctx.log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) });
|
||||
}
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
return null;
|
||||
}
|
||||
case 'sync':
|
||||
loginUrl = `${baseUrl}/rest/noauth/auth/password`;
|
||||
contentType = 'application/json';
|
||||
loginBody = JSON.stringify({ username, password });
|
||||
break;
|
||||
case 'chat':
|
||||
loginUrl = `${baseUrl}/api/v1/auths/signin`;
|
||||
contentType = 'application/json';
|
||||
loginBody = JSON.stringify({ email: username, password });
|
||||
expectJsonToken = true;
|
||||
break;
|
||||
case 'jellyfin':
|
||||
case 'emby': {
|
||||
const mediaAuth = buildMediaAuth(APP.DEVICE_IDS.SSO);
|
||||
try {
|
||||
const authResp = await ctx.fetchT(`${baseUrl}/Users/AuthenticateByName`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Emby-Authorization': mediaAuth },
|
||||
body: JSON.stringify({ Username: username, Pw: password }),
|
||||
}, TIMEOUTS.HTTP_LONG);
|
||||
const authData = await authResp.json();
|
||||
if (authData.AccessToken) {
|
||||
const tokenData = {
|
||||
token: authData.AccessToken, userId: authData.User?.Id,
|
||||
serverId: authData.ServerId, serverName: authData.User?.ServerName || serviceId,
|
||||
};
|
||||
appSessionCache.set(serviceId, { cookies: `token=${authData.AccessToken}`, token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
||||
ctx.log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId });
|
||||
return `token=${authData.AccessToken}`;
|
||||
}
|
||||
ctx.log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status });
|
||||
} catch (e) {
|
||||
ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
||||
}
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
return null;
|
||||
}
|
||||
case 'plex': {
|
||||
try {
|
||||
const plexResp = await ctx.fetchT(PLEX.AUTH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json', 'Content-Type': 'application/json',
|
||||
'Authorization': `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
||||
'X-Plex-Client-Identifier': APP.DEVICE_IDS.SSO,
|
||||
'X-Plex-Product': APP.NAME, 'X-Plex-Version': APP.VERSION,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}, TIMEOUTS.HTTP_LONG);
|
||||
const plexData = await plexResp.json();
|
||||
const token = plexData?.user?.authToken;
|
||||
if (token) {
|
||||
appSessionCache.set(serviceId, { cookies: `plexToken=${token}`, token, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
||||
ctx.log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId });
|
||||
return `plexToken=${token}`;
|
||||
}
|
||||
ctx.log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status });
|
||||
} catch (e) {
|
||||
ctx.log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message });
|
||||
}
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
loginUrl = `${baseUrl}login`;
|
||||
loginBody = `username=${formEncode(username)}&password=${formEncode(password)}&rememberMe=on`;
|
||||
extraHeaders['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await ctx.fetchT(loginUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': contentType, ...extraHeaders },
|
||||
body: loginBody, redirect: 'manual',
|
||||
}, TIMEOUTS.HTTP_LONG);
|
||||
|
||||
if (expectJsonToken) {
|
||||
try {
|
||||
const data = await resp.json();
|
||||
if (data.token) {
|
||||
const cookies = `token=${data.token}`;
|
||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||
ctx.log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId });
|
||||
return cookies;
|
||||
}
|
||||
} catch (e) { /* JSON parse failed */ }
|
||||
ctx.log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status });
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serviceId === 'torrent') {
|
||||
const text = await resp.text();
|
||||
if (text.trim() !== 'Ok.') {
|
||||
ctx.log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() });
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const setCookies = resp.headers.getSetCookie?.() || [];
|
||||
if (setCookies.length > 0) {
|
||||
const cookies = setCookies.map(c => c.split(';')[0]).join('; ');
|
||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||
ctx.log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length });
|
||||
return cookies;
|
||||
}
|
||||
|
||||
const rawCookie = resp.headers.get('set-cookie');
|
||||
if (rawCookie) {
|
||||
const cookies = rawCookie.split(/,(?=[^ ])/).map(c => c.split(';')[0].trim()).join('; ');
|
||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||
ctx.log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId });
|
||||
return cookies;
|
||||
}
|
||||
|
||||
ctx.log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status });
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
} catch (e) {
|
||||
ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose both the function and the cache so sso-gate can use them
|
||||
return { getAppSession, appSessionCache };
|
||||
};
|
||||
Reference in New Issue
Block a user