first commit

This commit is contained in:
2026-04-14 19:44:21 -05:00
commit 068576cf4b
36 changed files with 13680 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
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<IssuedRefreshToken> {
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
}
}