server: core bootstrap — config, db clients, shared utilities

Add env config (zod validation), game balance constants, Prisma and
Redis client singletons, auth guard middleware, error helpers,
request validators, Fastify JWT type augmentation, and app/server
entry points.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:41:33 +02:00
parent 8a91a298a7
commit 8ad83348f4
10 changed files with 346 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { errors } from './errors.js';
/** preHandler che richiede un JWT valido; popola request.user con { sub, playerId }. */
export async function authGuard(request: FastifyRequest, _reply: FastifyReply): Promise<void> {
try {
await request.jwtVerify();
} catch {
throw errors.unauthorized('Token mancante o non valido');
}
}
+23
View File
@@ -0,0 +1,23 @@
/** Errore applicativo con status HTTP e codice macchina-leggibile. */
export class AppError extends Error {
constructor(
public readonly statusCode: number,
public readonly code: string,
message: string,
) {
super(message);
this.name = 'AppError';
}
}
export const errors = {
badRequest: (message: string) => new AppError(400, 'BAD_REQUEST', message),
validation: (message: string) => new AppError(400, 'VALIDATION_ERROR', message),
unauthorized: (message = 'Non autenticato') => new AppError(401, 'UNAUTHORIZED', message),
forbidden: (message = 'Operazione non consentita') => new AppError(403, 'FORBIDDEN', message),
notFound: (message = 'Risorsa non trovata') => new AppError(404, 'NOT_FOUND', message),
conflict: (message: string) => new AppError(409, 'CONFLICT', message),
insufficientFunds: () => new AppError(400, 'INSUFFICIENT_FUNDS', 'Denaro insufficiente'),
insufficientItems: () =>
new AppError(400, 'INSUFFICIENT_ITEMS', 'Quantità insufficiente in inventario'),
};
+30
View File
@@ -0,0 +1,30 @@
import type { ZodTypeAny, output } from 'zod';
import { AppError } from './errors.js';
/** Valida `data` con lo schema Zod; lancia AppError 400 con i dettagli in caso di errore. */
export function parseOrThrow<S extends ZodTypeAny>(schema: S, data: unknown): output<S> {
const result = schema.safeParse(data);
if (!result.success) {
const detail = result.error.issues
.map((issue) => `${issue.path.join('.') || 'body'}: ${issue.message}`)
.join('; ');
throw new AppError(400, 'VALIDATION_ERROR', detail);
}
return result.data;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export function randomFloat(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
export function pickRandom<T>(values: readonly T[]): T {
return values[Math.floor(Math.random() * values.length)]!;
}