Files
AI_Video_Factory/server/src/lib/auth.ts
youbin 36f466ba11
Some checks failed
CI / Migrations, tests, build, and audit (push) Failing after 10m7s
CI / Production gate and container images (push) Successful in 20m35s
Release v1.0.0
2026-07-31 14:21:43 +08:00

101 lines
3.2 KiB
TypeScript

import { createHash, randomBytes } from 'node:crypto'
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import { and, eq, gt, isNull } from 'drizzle-orm'
import { config } from '../config.js'
import { db } from '../db/client.js'
import { refreshTokens, users } from '../db/schema.js'
import { errors } from './errors.js'
export const REFRESH_COOKIE = 'ff_refresh'
export function hashToken(value: string) {
return createHash('sha256').update(value).digest('hex')
}
export function generateRefreshToken() {
return randomBytes(48).toString('base64url')
}
export function refreshCookieOptions() {
return {
httpOnly: true,
secure: config.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/api/v1/auth',
maxAge: config.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60,
}
}
export function signAccessToken(app: FastifyInstance, user: { id: string; email: string; displayName: string }) {
return app.jwt.sign({
sub: user.id,
email: user.email,
displayName: user.displayName,
tokenType: 'access' as const,
}, { expiresIn: config.ACCESS_TOKEN_TTL })
}
export async function createRefreshSession(input: {
userId: string
userAgent?: string
ipAddress?: string
}) {
const token = generateRefreshToken()
const expiresAt = new Date(Date.now() + config.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000)
const [session] = await db.insert(refreshTokens).values({
userId: input.userId,
tokenHash: hashToken(token),
expiresAt,
userAgent: input.userAgent,
ipAddress: input.ipAddress,
}).returning()
if (!session) throw new Error('Failed to create refresh session')
return { token, session }
}
export async function rotateRefreshSession(token: string, meta: { userAgent?: string; ipAddress?: string }) {
const [current] = await db.select().from(refreshTokens).where(and(
eq(refreshTokens.tokenHash, hashToken(token)),
isNull(refreshTokens.revokedAt),
gt(refreshTokens.expiresAt, new Date()),
)).limit(1)
if (!current) throw errors.unauthorized('Refresh session is invalid or expired')
const [user] = await db.select({
id: users.id,
email: users.email,
displayName: users.displayName,
emailVerifiedAt: users.emailVerifiedAt,
disabledAt: users.disabledAt,
}).from(users).where(eq(users.id, current.userId)).limit(1)
if (!user || user.disabledAt) throw errors.unauthorized('Account is unavailable')
if (config.EMAIL_VERIFICATION_REQUIRED && !user.emailVerifiedAt) throw errors.emailVerificationRequired()
const next = await createRefreshSession({ userId: user.id, ...meta })
await db.update(refreshTokens).set({
revokedAt: new Date(),
replacedById: next.session.id,
}).where(eq(refreshTokens.id, current.id))
return { user, token: next.token }
}
export async function revokeRefreshSession(token?: string) {
if (!token) return
await db.update(refreshTokens).set({ revokedAt: new Date() }).where(and(
eq(refreshTokens.tokenHash, hashToken(token)),
isNull(refreshTokens.revokedAt),
))
}
export async function requireAuth(request: FastifyRequest, _reply: FastifyReply) {
try {
await request.jwtVerify()
} catch {
throw errors.unauthorized()
}
if (request.user.tokenType !== 'access') throw errors.unauthorized()
}