server: auth module — register, login, JWT
Implement registration and login endpoints with bcrypt password hashing and JWT token issuance; all game routes depend on this. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import type { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '../../db/prisma.js';
|
||||
import { authGuard } from '../../shared/authGuard.js';
|
||||
import { errors } from '../../shared/errors.js';
|
||||
import { parseOrThrow } from '../../shared/validators.js';
|
||||
import { toPlayerDto } from '../players/players.service.js';
|
||||
import { loginUser, registerUser } from './auth.service.js';
|
||||
|
||||
const registerSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8).max(128),
|
||||
displayName: z.string().min(3).max(20),
|
||||
});
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
/** Rate limit dedicato agli endpoint di autenticazione (vedi blueprint §11). */
|
||||
const authRateLimit = {
|
||||
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
|
||||
};
|
||||
|
||||
export const authRoutes: FastifyPluginAsync = async (app) => {
|
||||
app.post('/register', authRateLimit, async (request, reply) => {
|
||||
const input = parseOrThrow(registerSchema, request.body);
|
||||
const user = await registerUser(input);
|
||||
const accessToken = app.jwt.sign({ sub: user.id, playerId: user.player.id });
|
||||
request.log.info({ userId: user.id, playerId: user.player.id }, 'auth:register');
|
||||
return reply.status(201).send({ accessToken, player: toPlayerDto(user.player) });
|
||||
});
|
||||
|
||||
app.post('/login', authRateLimit, async (request) => {
|
||||
const input = parseOrThrow(loginSchema, request.body);
|
||||
const user = await loginUser(input);
|
||||
const accessToken = app.jwt.sign({ sub: user.id, playerId: user.player.id });
|
||||
request.log.info({ userId: user.id }, 'auth:login');
|
||||
return { accessToken, player: toPlayerDto(user.player) };
|
||||
});
|
||||
|
||||
app.get('/me', { preHandler: [authGuard] }, async (request) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: request.user.sub },
|
||||
include: { player: true },
|
||||
});
|
||||
if (!user?.player) throw errors.notFound('Utente non trovato');
|
||||
return {
|
||||
user: { id: user.id, email: user.email, createdAt: user.createdAt },
|
||||
player: toPlayerDto(user.player),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import argon2 from 'argon2';
|
||||
import type { Player, User } from '@prisma/client';
|
||||
import { balance } from '../../config/gameBalance.js';
|
||||
import { prisma } from '../../db/prisma.js';
|
||||
import { AppError, errors } from '../../shared/errors.js';
|
||||
|
||||
export type UserWithPlayer = User & { player: Player };
|
||||
|
||||
export async function registerUser(input: {
|
||||
email: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
}): Promise<UserWithPlayer> {
|
||||
const existingEmail = await prisma.user.findUnique({ where: { email: input.email } });
|
||||
if (existingEmail) throw errors.conflict('Email già registrata');
|
||||
|
||||
const existingName = await prisma.player.findUnique({
|
||||
where: { displayName: input.displayName },
|
||||
});
|
||||
if (existingName) throw errors.conflict('Nome giocatore già in uso');
|
||||
|
||||
const startingCity =
|
||||
(await prisma.city.findUnique({ where: { name: balance.player.startingCityName } })) ??
|
||||
(await prisma.city.findFirst({ orderBy: { name: 'asc' } }));
|
||||
if (!startingCity) {
|
||||
throw new AppError(503, 'WORLD_NOT_SEEDED', 'Mondo di gioco non inizializzato: eseguire il seed');
|
||||
}
|
||||
|
||||
const passwordHash = await argon2.hash(input.password);
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: input.email,
|
||||
passwordHash,
|
||||
player: {
|
||||
create: { displayName: input.displayName, currentCityId: startingCity.id },
|
||||
},
|
||||
},
|
||||
include: { player: true },
|
||||
});
|
||||
return user as UserWithPlayer;
|
||||
}
|
||||
|
||||
export async function loginUser(input: {
|
||||
email: string;
|
||||
password: string;
|
||||
}): Promise<UserWithPlayer> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: input.email },
|
||||
include: { player: true },
|
||||
});
|
||||
if (!user || !user.player) throw errors.unauthorized('Credenziali non valide');
|
||||
|
||||
const passwordOk = await argon2.verify(user.passwordHash, input.password);
|
||||
if (!passwordOk) throw errors.unauthorized('Credenziali non valide');
|
||||
|
||||
return user as UserWithPlayer;
|
||||
}
|
||||
Reference in New Issue
Block a user