import { Prisma } from '@prisma/client' import { createError, readBody, setResponseStatus, type H3Event } from 'h3' import { signAccessToken } from './jwt' import { getAuthRuntimeConfig } from './auth-config' import { hashPassword, verifyPassword } from './password' import { prisma } from './prisma' import { issueRefreshToken, rotateRefreshToken } from './refresh-token' interface LoginBody { email?: unknown password?: unknown } interface RefreshBody { refresh_token?: unknown } interface RegisterBody { email?: unknown password?: unknown } /** * Valida e normaliza email/senha enviados no login. * * @param body Corpo bruto da requisição de login. * @returns Email normalizado e senha pronta para validação. * @throws {H3Error} Quando email ou senha não forem informados. */ function parseCredentialBody(body: LoginBody) { const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '' const password = typeof body.password === 'string' ? body.password : '' if (!email || !password) { throw createError({ statusCode: 400, statusMessage: 'email and password are required' }) } return { email, password } } /** * Valida os dados de cadastro e aplica regra mínima de senha. * * @param body Corpo bruto da requisição de cadastro. * @returns Email normalizado e senha pronta para persistência. * @throws {H3Error} Quando os dados forem inválidos. */ function parseRegisterBody(body: RegisterBody) { const { email, password } = parseCredentialBody(body) if (password.length < 6) { throw createError({ statusCode: 400, statusMessage: 'password must have at least 6 characters' }) } return { email, password } } /** * Valida o refresh token enviado no corpo da requisição. * * @param body Corpo bruto da requisição de refresh. * @returns Refresh token em formato de string. * @throws {H3Error} Quando `refresh_token` não for informado. */ function parseRefreshBody(body: RefreshBody): string { const refreshToken = typeof body.refresh_token === 'string' ? body.refresh_token.trim() : '' if (!refreshToken) { throw createError({ statusCode: 400, statusMessage: 'refresh_token is required' }) } return refreshToken } /** * Executa o fluxo de cadastro: * valida entrada, cria usuário e retorna dados básicos sem autenticar. * * @param event Evento HTTP da requisição. * @returns Usuário criado no formato público. * @throws {H3Error} Quando houver dados inválidos ou email já cadastrado. */ export async function handleRegister(event: H3Event) { const body = await readBody(event) const { email, password } = parseRegisterBody(body ?? {}) try { const createdUser = await prisma.user.create({ data: { email, passwordHash: hashPassword(password) }, select: { id: true, email: true, createdAt: true, updatedAt: true } }) setResponseStatus(event, 201) return { id: createdUser.id, email: createdUser.email, created_at: createdUser.createdAt.toISOString(), updated_at: createdUser.updatedAt.toISOString() } } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' && Array.isArray(error.meta?.target) && error.meta.target.includes('email') ) { throw createError({ statusCode: 409, statusMessage: 'Email já cadastrado' }) } throw error } } /** * Executa o fluxo de login: * valida credenciais, gera access token e emite refresh token. * * @param event Evento HTTP da requisição. * @returns Resposta padrão de autenticação para o cliente. * @throws {H3Error} Quando as credenciais forem inválidas ou faltarem dados. */ export async function handleLogin(event: H3Event) { const config = getAuthRuntimeConfig(event) const body = await readBody(event) const { email, password } = parseCredentialBody(body ?? {}) const user = await prisma.user.findUnique({ where: { email } }) if (!user || !verifyPassword(password, user.passwordHash)) { throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' }) } const accessToken = await signAccessToken(event, { sub: user.id }) const refreshToken = await issueRefreshToken(event, user.id) return { access_token: accessToken, refresh_token: refreshToken.token, token_type: 'Bearer', expires_in: config.accessTtlSec } } /** * Executa o fluxo de refresh: * valida o refresh token, rotaciona e retorna novo access token. * * @param event Evento HTTP da requisição. * @returns Novo par de tokens para continuar a sessão. * @throws {H3Error} Quando o refresh token for inválido ou ausente. */ export async function handleRefresh(event: H3Event) { const config = getAuthRuntimeConfig(event) const body = await readBody(event) const incomingRefreshToken = parseRefreshBody(body ?? {}) const rotated = await rotateRefreshToken(event, incomingRefreshToken) const accessToken = await signAccessToken(event, { sub: rotated.user.id }) return { access_token: accessToken, refresh_token: rotated.token, token_type: 'Bearer', expires_in: config.accessTtlSec } }