feat(rednote): add isolated persistent browser H5 entry
This commit is contained in:
+25
-11
@@ -11,6 +11,9 @@ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
||||
// One Node process owns this embedded store. No credentials or identity are accepted
|
||||
// from a report request; the owner always comes from a server-created session.
|
||||
module.exports = function createAccounts(root, env = process.env) {
|
||||
const rednote = env.JUZI_ENTRY_MODE === 'rednote';
|
||||
const sessionCookie = rednote ? 'juzi_rednote_session' : 'juzi_session';
|
||||
const persistentKind = kind => ['wechat', 'taobao'].includes(kind) || (rednote && kind === 'browser_persistent');
|
||||
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
||||
const dbFile = path.join(root, 'identities.json');
|
||||
const snapshots = path.join(root, 'reports');
|
||||
@@ -32,7 +35,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
const guestSessions = new Map(); // No guest identities/sessions are written to disk.
|
||||
const appId = env.WECHAT_APP_ID || '';
|
||||
const secret = env.WECHAT_APP_SECRET || '';
|
||||
const available = !!(appId && secret && secure && !/^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/.test(base.hostname));
|
||||
const available = !rednote && !!(appId && secret && secure && !/^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/.test(base.hostname));
|
||||
function cookies(req) {
|
||||
return Object.fromEntries((req.headers.cookie || '').split(';').map(s => s.trim().split('=')));
|
||||
}
|
||||
@@ -42,17 +45,17 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
res.setHeader('Set-Cookie', [].concat(old, line));
|
||||
}
|
||||
function session(req) {
|
||||
const token = cookies(req).juzi_session;
|
||||
const token = cookies(req)[sessionCookie];
|
||||
const guest = token && guestSessions.get(hash(token));
|
||||
if (guest && guest.expires > Date.now()) return guest.user;
|
||||
const row = token && db.sessions[hash(token)];
|
||||
return row && row.expires > Date.now() ? db.users.find(u => u.id === row.user && u.kind === 'wechat') : null;
|
||||
return row && row.expires > Date.now() ? db.users.find(u => u.id === row.user && u.kind === (rednote ? 'browser_persistent' : 'wechat')) : null;
|
||||
}
|
||||
function login(res, user) {
|
||||
for (const key of Object.keys(db.sessions)) if (db.sessions[key].expires < Date.now()) delete db.sessions[key];
|
||||
const token = random();
|
||||
db.sessions[hash(token)] = { user: user.id, expires: Date.now() + 30 * 86400000 };
|
||||
cookie(res, 'juzi_session', token, 30 * 86400);
|
||||
cookie(res, sessionCookie, token, 30 * 86400);
|
||||
save();
|
||||
}
|
||||
function publicUser(u) { return u ? { id: u.id, kind: u.kind } : null; }
|
||||
@@ -91,6 +94,9 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
if (!pathname.startsWith('/api/auth/') && !pathname.startsWith('/api/reports')) return false;
|
||||
try {
|
||||
if (req.method !== 'GET' && !sameOrigin(req)) { json(res, 403, { ok: false, message: '请求来源不匹配' }); return true; }
|
||||
if (rednote && pathname.startsWith('/api/auth/wechat')) {
|
||||
json(res, 404, { ok: false, message: '此入口不提供微信登录' }); return true;
|
||||
}
|
||||
if (pathname === '/api/auth/config' && req.method === 'GET') {
|
||||
json(res, 200, { ok: true, wechat: available, fallback: 'browser', reason: available ? null : '当前环境尚未开通微信登录,可使用游客登录' });
|
||||
} else if (pathname === '/api/auth/session' && req.method === 'GET') {
|
||||
@@ -101,20 +107,28 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; }
|
||||
// Old persistent guest tokens cannot restore identity or history.
|
||||
if (data.token) { json(res, 401, { ok: false, message: '游客身份已失效,请重新登录' }); return true; }
|
||||
if (rednote) {
|
||||
let user = session(req);
|
||||
if (!user) {
|
||||
user = { id: crypto.randomUUID(), kind: 'browser_persistent', channel: 'rednote', consent: new Date().toISOString(), reports: [] };
|
||||
db.users.push(user); login(res, user);
|
||||
}
|
||||
json(res, 200, { ok: true, user: publicUser(user) }); return true;
|
||||
}
|
||||
for (const [key, value] of guestSessions) if (value.expires < Date.now()) guestSessions.delete(key);
|
||||
const previous = cookies(req).juzi_session;
|
||||
const previous = cookies(req)[sessionCookie];
|
||||
if (previous) guestSessions.delete(hash(previous));
|
||||
const token = random(), user = { id: crypto.randomUUID(), kind: 'browser' };
|
||||
guestSessions.set(hash(token), { user, expires: Date.now() + 8 * 3600000 });
|
||||
cookie(res, 'juzi_session', token, null);
|
||||
cookie(res, sessionCookie, token, null);
|
||||
json(res, 200, { ok: true, user: publicUser(user) });
|
||||
} else if (pathname === '/api/auth/logout' && req.method === 'POST') {
|
||||
const token = cookies(req).juzi_session;
|
||||
const token = cookies(req)[sessionCookie];
|
||||
if (token) {
|
||||
guestSessions.delete(hash(token));
|
||||
if (db.sessions[hash(token)]) { delete db.sessions[hash(token)]; save(); }
|
||||
}
|
||||
cookie(res, 'juzi_session', '', 0); json(res, 200, { ok: true });
|
||||
cookie(res, sessionCookie, '', 0); json(res, 200, { ok: true });
|
||||
} else if (pathname === '/api/auth/wechat' && req.method === 'POST') {
|
||||
const data = await body(req);
|
||||
if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; }
|
||||
@@ -155,7 +169,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
const user = session(req);
|
||||
if (!user) { json(res, 401, { ok: false, message: '请先登录' }); return true; }
|
||||
const id = pathname.slice('/api/reports/'.length);
|
||||
if (user.kind !== 'wechat') {
|
||||
if (user.kind !== 'wechat' && !(rednote && user.kind === 'browser_persistent')) {
|
||||
json(res, pathname === '/api/reports' ? 200 : 404,
|
||||
pathname === '/api/reports' ? { ok: true, total: 0, items: [] } : { ok: false, message: '游客模式不保存历史报告' });
|
||||
return true;
|
||||
@@ -177,7 +191,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
}
|
||||
function storeReport(ownerId, snapshot) {
|
||||
const user = db.users.find(u => u.id === ownerId);
|
||||
if (!user || !['wechat', 'taobao'].includes(user.kind) || !/^R[\w-]+$/.test(snapshot.id)) throw new Error('Invalid report owner');
|
||||
if (!user || !persistentKind(user.kind) || !/^R[\w-]+$/.test(snapshot.id)) throw new Error('Invalid report owner');
|
||||
const dir = path.join(snapshots, user.id); fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
atomic(path.join(dir, snapshot.id + '.json'), snapshot);
|
||||
user.reports = [{ id: snapshot.id, created_at: snapshot.created_at, total: snapshot.vm.total, skinAge: snapshot.vm.skinAgeDisplay }]
|
||||
@@ -193,7 +207,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
catch (_) { return null; }
|
||||
}
|
||||
function reportSnapshot(ownerId,id) {
|
||||
if (!db.users.some(u=>u.id===ownerId&&['wechat','taobao'].includes(u.kind)) || !/^R[\w-]+$/.test(id)) return null;
|
||||
if (!db.users.some(u=>u.id===ownerId&&persistentKind(u.kind)) || !/^R[\w-]+$/.test(id)) return null;
|
||||
try {return JSON.parse(fs.readFileSync(path.join(snapshots,ownerId,id+'.json'),'utf8'));} catch (_) {return null;}
|
||||
}
|
||||
function updateWrinkleViews(ownerId,id,views) {
|
||||
|
||||
Reference in New Issue
Block a user