This commit is contained in:
2026-09-12 10:10:34 +02:00
parent 217f1be72d
commit 53055ea5c6
35 changed files with 2261 additions and 138 deletions
+129
View File
@@ -0,0 +1,129 @@
import db from '../../sql.mjs';
import lib from '../../lib.mjs';
import cfg from '../../config.mjs';
import { parseOpenSshPubkey, verifySignature, getOrCreateAnonUser, createAnonSession } from '../../anon_auth.mjs';
import { getEnableAnonymousAccess } from '../../settings.mjs';
export default router => {
router.group(/^\/api\/v2\/anon/, group => {
/**
* POST /api/v2/anon/session
* Authenticate via OpenSSH Ed25519 signature and establish an anonymous session.
*/
group.post(/\/session$/, async (req, res) => {
try {
if (!getEnableAnonymousAccess()) {
return res.json({ success: false, msg: 'Anonymous access is disabled' }, 403);
}
const body = req.post || req.body || {};
const pubkey = (body.pubkey || '').trim();
const timestamp = parseInt(body.timestamp, 10);
const signature = (body.signature || '').trim();
if (!pubkey || !timestamp || !signature) {
return res.json({ success: false, msg: 'Missing pubkey, timestamp, or signature' }, 400);
}
// Freshness check (5-minute window for clock skew)
const now = Date.now();
if (Math.abs(now - timestamp) > 300000) {
return res.json({ success: false, msg: 'Timestamp expired or out of bounds' }, 401);
}
const message = `anon-auth:${timestamp}:${pubkey}`;
const isValid = verifySignature(pubkey, message, signature);
if (!isValid) {
return res.json({ success: false, msg: 'Invalid Ed25519 signature' }, 401);
}
const parsed = parseOpenSshPubkey(pubkey);
const { userId, isNew } = await getOrCreateAnonUser(pubkey, parsed.fingerprint);
const { session, csrf_token } = await createAnonSession(userId, req);
res.setHeader('Set-Cookie', `session=${session}; ${lib.getCookieOptions('Fri, 31 Dec 9999 23:59:59 GMT')}`);
return res.json({
success: true,
is_new: isNew,
user_id: userId,
fingerprint: parsed.fingerprint,
short_fingerprint: parsed.shortFingerprint,
csrf_token: csrf_token
});
} catch (err) {
console.error('[ANON_AUTH] Session establishment error:', err);
return res.json({ success: false, msg: err.message || 'Internal server error' }, 500);
}
});
/**
* GET /api/v2/anon/identity
* Get the current anonymous identity or registered user state.
*/
group.get(/\/identity$/, async (req, res) => {
try {
if (!getEnableAnonymousAccess()) {
return res.json({ logged_in: false, is_anon: false, disabled: true });
}
if (!req.session) {
return res.json({ logged_in: false, is_anon: false });
}
const rows = await db`
SELECT pubkey, fingerprint, created_at, last_seen
FROM anon_identities
WHERE user_id = ${req.session.id}
LIMIT 1
`;
if (rows.length > 0) {
const fp = rows[0].fingerprint;
return res.json({
logged_in: true,
is_anon: true,
user_id: req.session.id,
fingerprint: fp,
short_fingerprint: fp.slice(7, 15),
pubkey: rows[0].pubkey,
csrf_token: req.session.csrf_token
});
}
return res.json({
logged_in: true,
is_anon: false,
user: req.session.user,
user_id: req.session.id,
csrf_token: req.session.csrf_token
});
} catch (err) {
console.error('[ANON_AUTH] Identity lookup error:', err);
return res.json({ success: false, msg: err.message }, 500);
}
});
/**
* POST /api/v2/anon/logout
* Clear anonymous session cookie and remove active session from database.
*/
group.post(/\/logout$/, async (req, res) => {
try {
if (req.session && req.session.sess_id) {
await db`
DELETE FROM user_sessions
WHERE id = ${+req.session.sess_id}
`;
}
res.setHeader('Set-Cookie', `session=; ${lib.getCookieOptions('Thu, 01 Jan 1970 00:00:00 GMT')}`);
return res.json({ success: true });
} catch (err) {
console.error('[ANON_AUTH] Logout error:', err);
return res.json({ success: false, msg: err.message }, 500);
}
});
});
};