Changed sign-up flow to require e-mail verification up front, preventing account existence leakage.

This commit is contained in:
2026-07-23 22:12:33 +02:00
parent 18c30b6f98
commit 6b3866a99f
12 changed files with 387 additions and 79 deletions

View File

@@ -13,6 +13,7 @@ import {
resetPassword,
sendEmailVerification,
sendPasswordReset,
sendSignup,
signup,
updateSettings,
updateUser,
@@ -23,25 +24,20 @@ import {
fetchUser,
} from '../users.js';
// changes every time the process restarts, so a deploy invalidates all cached session users
// even if they're within the staleness window below
const sessionStaleThreshold = 5 * 60 * 1000;
// spread into a new object rather than mutating the user we're about to hand back to the
// client (loginApi/signupApi send this straight to the browser) — runtimeId/refreshedAt are
// session bookkeeping only, they shouldn't leak into the API response
function toSession(user) {
if (!user) {
return null;
}
return {
...user,
runtimeId,
refreshedAt: Date.now(),
};
}
function isSessionStale(sessionUser) {
return sessionUser.runtimeId !== runtimeId || Date.now() - sessionUser.refreshedAt > sessionStaleThreshold;
}
// concurrent requests for the same user (e.g. the burst of asset/API requests a single page
// load fires) share one in-flight refresh instead of each independently hitting the DB
const pendingSessionRefreshes = new Map();
@@ -90,16 +86,24 @@ function getIp(req) {
return expandedIp.addressStart?.addressMinusSuffix || null;
}
// changes every time the process restarts, so a deploy invalidates all cached session users
// even if they're within the staleness window below
const sessionStaleThreshold = 5 * 60 * 1000;
function isSessionStale(sessionUser) {
return !!sessionUser && (sessionUser.runtimeId !== runtimeId || Date.now() - sessionUser.refreshedAt > sessionStaleThreshold);
}
export async function setUserApi(req, res, next) {
const ip = getIp(req);
req.userIp = ip;
if (req.session.user) {
if (isSessionStale(req.session.user)) {
req.session.user = await refreshSessionUser(req.session.user);
}
if (isSessionStale(req.session.user)) {
req.session.user = await refreshSessionUser(req.session.user);
}
if (req.session.user) {
req.user = req.session.user;
req.user.ip = ip;
}
@@ -110,10 +114,12 @@ export async function setUserApi(req, res, next) {
export async function updateSessionUser(req) {
const user = toSession(await fetchUser(req.session.user.id, {}, req.session.user));
req.session.user = user;
if (user) {
req.session.user = user;
req.user = user;
req.user.ip = req.userIp;
req.user = user;
req.user.ip = req.userIp;
}
}
export async function loginApi(req, res) {
@@ -133,6 +139,12 @@ export async function logoutApi(req, res) {
});
}
async function sendSignupApi(req, res) {
await sendSignup(req.body.email, req.userIp);
res.status(204).send();
}
export async function signupApi(req, res) {
const user = await signup(req.body, req.userIp);
@@ -241,6 +253,7 @@ authRouter.post('/api/session', loginApi);
authRouter.delete('/api/session', logoutApi);
// USERS
authRouter.post('/api/signups', sendSignupApi);
authRouter.post('/api/users', signupApi);
// SETTINGS