phase 1 of cleanup: move GET health, GET auth/session, and PATCH endpoints
This commit is contained in:
101
api/src/routes/session.ts
Normal file
101
api/src/routes/session.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user