Files
SkyMoney/api/src/routes/session.ts
Ricearoni1245 60cdcf1fcf
All checks were successful
Deploy / deploy (push) Successful in 1m27s
Security Tests / security-non-db (push) Successful in 20s
Security Tests / security-db (push) Successful in 25s
phase 1 of cleanup: move GET health, GET auth/session, and PATCH endpoints
2026-03-15 15:05:38 -05:00

102 lines
2.6 KiB
TypeScript

import type { FastifyPluginAsync } from "fastify";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import type { AppConfig } from "../server.js";
type SessionRoutesOptions = {
config: Pick<
AppConfig,
"NODE_ENV" | "UPDATE_NOTICE_VERSION" | "UPDATE_NOTICE_TITLE" | "UPDATE_NOTICE_BODY"
>;
cookieDomain?: string;
mutationRateLimit: {
config: {
rateLimit: {
max: number;
timeWindow: number;
};
};
};
};
const CSRF_COOKIE = "csrf";
const sessionRoutes: FastifyPluginAsync<SessionRoutesOptions> = async (
app,
opts
) => {
const ensureCsrfCookie = (reply: any, existing?: string) => {
const token = existing ?? randomUUID().replace(/-/g, "");
reply.setCookie(CSRF_COOKIE, token, {
httpOnly: false,
sameSite: "lax",
secure: opts.config.NODE_ENV === "production",
path: "/",
...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}),
});
return token;
};
app.get("/auth/session", async (req, reply) => {
if (!(req.cookies as any)?.[CSRF_COOKIE]) {
ensureCsrfCookie(reply);
}
const user = await app.prisma.user.findUnique({
where: { id: req.userId },
select: {
email: true,
displayName: true,
emailVerified: true,
seenUpdateVersion: true,
},
});
const noticeVersion = opts.config.UPDATE_NOTICE_VERSION;
const shouldShowNotice =
noticeVersion > 0 &&
!!user &&
user.emailVerified &&
user.seenUpdateVersion < noticeVersion;
return {
ok: true,
userId: req.userId,
email: user?.email ?? null,
displayName: user?.displayName ?? null,
emailVerified: user?.emailVerified ?? false,
updateNotice: shouldShowNotice
? {
version: noticeVersion,
title: opts.config.UPDATE_NOTICE_TITLE,
body: opts.config.UPDATE_NOTICE_BODY,
}
: null,
};
});
app.post(
"/app/update-notice/ack",
opts.mutationRateLimit,
async (req, reply) => {
const Body = z.object({
version: z.coerce.number().int().nonnegative().optional(),
});
const parsed = Body.safeParse(req.body);
if (!parsed.success) {
return reply.code(400).send({ ok: false, message: "Invalid payload" });
}
const targetVersion =
parsed.data.version ?? opts.config.UPDATE_NOTICE_VERSION;
await app.prisma.user.updateMany({
where: {
id: req.userId,
seenUpdateVersion: { lt: targetVersion },
},
data: { seenUpdateVersion: targetVersion },
});
return { ok: true };
}
);
};
export default sessionRoutes;