import { createHash, randomBytes } from "node:crypto" import jwt from "jsonwebtoken" import { FastifyInstance } from "fastify" import { and, eq, gt, isNull } from "drizzle-orm" import { authRefreshTokens, authUsers } from "../../db/schema" import { secrets } from "./secrets" const ACCESS_TOKEN_TTL_SECONDS = 60 * 60 * 6 const REFRESH_TOKEN_TTL_DAYS = 90 export function createAccessToken(user: { id: string; email: string }, tenantId: number | null) { return jwt.sign( { user_id: user.id, email: user.email, tenant_id: tenantId, }, secrets.JWT_SECRET!, { expiresIn: ACCESS_TOKEN_TTL_SECONDS } ) } export function hashRefreshToken(token: string) { return createHash("sha256").update(token, "utf8").digest("hex") } export async function issueRefreshToken( server: FastifyInstance, userId: string, tenantId: number | null ) { const token = randomBytes(48).toString("base64url") const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000) await server.db.insert(authRefreshTokens).values({ userId, tenantId, tokenHash: hashRefreshToken(token), expiresAt, }) return token } export async function rotateRefreshToken( server: FastifyInstance, token: string, tenantOverride?: number | null, expectedUserId?: string ) { const tokenHash = hashRefreshToken(token) const [session] = await server.db .update(authRefreshTokens) .set({ revokedAt: new Date() }) .where(and( eq(authRefreshTokens.tokenHash, tokenHash), isNull(authRefreshTokens.revokedAt), gt(authRefreshTokens.expiresAt, new Date()) )) .returning({ id: authRefreshTokens.id, userId: authRefreshTokens.userId, tenantId: authRefreshTokens.tenantId, }) if (!session) return null if (expectedUserId && session.userId !== expectedUserId) return null const [user] = await server.db .select({ id: authUsers.id, email: authUsers.email }) .from(authUsers) .where(and(eq(authUsers.id, session.userId), isNull(authUsers.archived_at))) .limit(1) if (!user) return null const tenantId = tenantOverride === undefined ? session.tenantId : tenantOverride const refreshToken = await issueRefreshToken(server, session.userId, tenantId) const accessToken = createAccessToken(user, tenantId) return { token: accessToken, refreshToken } } export async function revokeRefreshToken(server: FastifyInstance, token: string) { await server.db .update(authRefreshTokens) .set({ revokedAt: new Date() }) .where(and( eq(authRefreshTokens.tokenHash, hashRefreshToken(token)), isNull(authRefreshTokens.revokedAt) )) }