94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
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)
|
|
))
|
|
}
|