import { createHash, randomBytes } from 'node:crypto' import { createError, type H3Event } from 'h3' import { getAuthRuntimeConfig } from './auth-config' import { prisma } from './prisma' export interface IssuedRefreshToken { token: string expiresAt: Date expiresInSec: number recordId: string } /** * Gera um hash seguro do refresh token usando pepper do ambiente. * O token bruto nunca é salvo no banco. * * @param rawToken Refresh token em texto puro. * @param pepper Segredo adicional do servidor. * @returns Hash SHA-256 do token. */ export function hashRefreshToken(rawToken: string, pepper: string): string { return createHash('sha256').update(`${rawToken}${pepper}`).digest('hex') } /** * Cria um refresh token aleatório para entregar ao cliente. * * @returns Token aleatório em formato URL-safe. */ function generateRawRefreshToken(): string { return randomBytes(48).toString('base64url') } /** * Emite e persiste um novo refresh token para um usuário. * * @param event Evento HTTP usado para ler config. * @param userId ID do usuário dono do token. * @returns Metadados do refresh token emitido. */ export async function issueRefreshToken(event: H3Event, userId: string): Promise { const config = getAuthRuntimeConfig(event) const token = generateRawRefreshToken() const tokenHash = hashRefreshToken(token, config.refreshTokenPepper) const expiresAt = new Date(Date.now() + config.refreshTtlSec * 1000) const created = await prisma.refreshToken.create({ data: { userId, tokenHash, expiresAt } }) return { token, expiresAt, expiresInSec: config.refreshTtlSec, recordId: created.id } } /** * Rotaciona um refresh token válido: * cria um novo token e marca o antigo como revogado. * * @param event Evento HTTP usado para ler config. * @param rawRefreshToken Token atual enviado pelo cliente. * @returns Dados do novo refresh token e usuário autenticado. * @throws {H3Error} Quando o token for inválido, revogado ou expirado. */ export async function rotateRefreshToken(event: H3Event, rawRefreshToken: string) { const config = getAuthRuntimeConfig(event) const tokenHash = hashRefreshToken(rawRefreshToken, config.refreshTokenPepper) const existing = await prisma.refreshToken.findUnique({ where: { tokenHash }, include: { user: true } }) if (!existing || existing.revokedAt || existing.expiresAt.getTime() <= Date.now()) { throw createError({ statusCode: 401, statusMessage: 'Invalid refresh token' }) } const newRawRefreshToken = generateRawRefreshToken() const newTokenHash = hashRefreshToken(newRawRefreshToken, config.refreshTokenPepper) const newExpiresAt = new Date(Date.now() + config.refreshTtlSec * 1000) const rotated = await prisma.$transaction(async (tx) => { const created = await tx.refreshToken.create({ data: { userId: existing.userId, tokenHash: newTokenHash, expiresAt: newExpiresAt } }) await tx.refreshToken.update({ where: { id: existing.id }, data: { revokedAt: new Date(), replacedById: created.id } }) return created }) return { user: existing.user, token: newRawRefreshToken, expiresAt: rotated.expiresAt, expiresInSec: config.refreshTtlSec, recordId: rotated.id, replacedTokenId: existing.id } }