diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts new file mode 100644 index 0000000..a87c7e9 --- /dev/null +++ b/server/api/auth/register.post.ts @@ -0,0 +1,9 @@ +import { handleRegister } from '../../utils/auth-service' + +/** + * Endpoint de cadastro público do Auth Service. + * Cria usuário com email único e não gera tokens neste fluxo. + */ +export default defineEventHandler(async (event) => { + return handleRegister(event) +}) diff --git a/server/routes/auth/register.post.ts b/server/routes/auth/register.post.ts new file mode 100644 index 0000000..d1406f8 --- /dev/null +++ b/server/routes/auth/register.post.ts @@ -0,0 +1,3 @@ +import registerHandler from '../../api/auth/register.post' + +export default registerHandler diff --git a/server/utils/auth-service.ts b/server/utils/auth-service.ts index 2750eac..c8318b0 100644 --- a/server/utils/auth-service.ts +++ b/server/utils/auth-service.ts @@ -1,8 +1,9 @@ -import { createError, readBody, type H3Event } from 'h3' +import { Prisma } from '@prisma/client' +import { createError, readBody, setResponseStatus, type H3Event } from 'h3' import { signAccessToken } from './jwt' import { getAuthRuntimeConfig } from './auth-config' -import { verifyPassword } from './password' +import { hashPassword, verifyPassword } from './password' import { prisma } from './prisma' import { issueRefreshToken, rotateRefreshToken } from './refresh-token' @@ -15,6 +16,11 @@ interface RefreshBody { refresh_token?: unknown } +interface RegisterBody { + email?: unknown + password?: unknown +} + /** * Valida e normaliza email/senha enviados no login. * @@ -33,6 +39,26 @@ function parseCredentialBody(body: LoginBody) { 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. * @@ -50,6 +76,57 @@ function parseRefreshBody(body: RefreshBody): string { 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.