102 lines
2.6 KiB
TypeScript
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;
|