lib/account-remembered.js
Rama main · réplica de lectura del repositorio original.
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var AGE = 90 * 86400000;
function attachRemembered(ctx) {
var name = ctx.devMode ? 'clay_accounts_dev' : 'clay_accounts';
function digest(token) { return crypto.createHash('sha256').update(token).digest('hex'); }
function token(req) { var value = ctx.parseCookies(req)[name]; return /^[a-f0-9]{64}$/.test(value || '') ? value : null; }
function load() {
try {
var data = JSON.parse(fs.readFileSync(ctx.file, 'utf8'));
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('Invalid remembered account data.');
Object.keys(data).forEach(function (key) {
var entry = data[key];
if (!/^[a-f0-9]{64}$/.test(key) || !entry || !Number.isFinite(entry.until) || !Array.isArray(entry.ids)) delete data[key];
});
return data;
}
catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
}
function save(data) {
Object.keys(data).forEach(function (key) { if (!data[key] || data[key].until <= Date.now()) delete data[key]; });
fs.mkdirSync(path.dirname(ctx.file), { recursive: true });
fs.writeFileSync(ctx.file + '.tmp', JSON.stringify(data), { mode: 0o600 }); fs.renameSync(ctx.file + '.tmp', ctx.file);
}
function cookie(value, req) {
return name + '=' + value + '; Path=/; HttpOnly; SameSite=Strict; Max-Age=' + (value ? AGE / 1000 : 0) + (ctx.transport.info(req).secure ? '; Secure' : '');
}
function remember(userId, req) {
if (!req) return null;
// Remembering a name is optional; a storage failure must not fail a valid login.
try {
var data = load(); var previous = token(req); var old = previous && data[digest(previous)];
var ids = old && old.until > Date.now() && Array.isArray(old.ids) ? old.ids : [];
var next = crypto.randomBytes(32).toString('hex');
if (previous) delete data[digest(previous)];
data[digest(next)] = { until: Date.now() + AGE, ids: [userId].concat(ids.filter(function (id) { return id !== userId; })).slice(0, 5) };
var keys = Object.keys(data).sort(function (a, b) { return data[a].until - data[b].until; });
keys.slice(0, Math.max(0, keys.length - 10000)).forEach(function (key) { delete data[key]; });
save(data); return cookie(next, req);
} catch (error) { return null; }
}
function accounts(req) {
var value = token(req); var entry = value && load()[digest(value)];
if (!entry || entry.until <= Date.now() || !Array.isArray(entry.ids)) return [];
return entry.ids.map(function (id) { return ctx.users.findUserById(id); }).filter(Boolean).map(function (user) {
return { id: user.id, username: user.username, name: user.profile && user.profile.name || user.displayName || user.username };
});
}
function handleRequest(req, res, url) {
if (url !== '/auth/accounts') return false;
function reply(code, data, cookies) { var headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }; if (cookies) headers['Set-Cookie'] = cookies; res.writeHead(code, headers); res.end(JSON.stringify(data)); }
if (!ctx.users.isMultiUser()) { reply(200, { accounts: [] }); return true; }
var origin = ctx.transport.info(req).origin;
if ((req.headers.origin && req.headers.origin !== origin) || req.headers['sec-fetch-site'] === 'cross-site') { reply(403, { error: 'Use the same Clay address.' }); return true; }
if (req.method === 'GET') {
try { reply(200, { accounts: accounts(req) }); } catch (error) { reply(503, { error: 'Remembered accounts are unavailable. You can still sign in.' }); }
return true;
}
if (req.method !== 'POST') { reply(405, { error: 'Method not allowed.' }); return true; }
if (!origin || req.headers.origin !== origin) { reply(403, { error: 'Use the same Clay address.' }); return true; }
ctx.read(req, res, function (body) {
if (typeof body.forget !== 'string' || !body.forget) { reply(400, { error: 'Choose an account to forget.' }); return; }
try {
var value = token(req); var data = load(); var key = value && digest(value); var entry = key && data[key];
if (entry) {
entry.ids = entry.ids.filter(function (id) { return id !== body.forget; });
if (!entry.ids.length) delete data[key];
save(data);
}
reply(200, { accounts: accounts(req) }, !entry || !entry.ids.length ? cookie('', req) : undefined);
} catch (error) { reply(503, { error: 'Could not forget this account. Please try again.' }); }
}); return true;
}
return { remember: remember, handleRequest: handleRequest };
}
module.exports = { attachRemembered: attachRemembered };