batchCooking/apps/api/src/modules/auth/auth.service.ts
Nicolas 1d03effc77 API: endpoints foyer/profil (nom, régime, allergènes) (step 2/6)
- GET/PATCH /house/current — renomme le foyer de l'utilisateur connecté.
  PATCH avec houseId null -> 404 HOUSE_NOT_FOUND.
- PATCH /profile/diet { dietId: number | null } — régime du profil ;
  null l'efface (étape skippable du parcours). dietId invalide ->
  404 DIET_NOT_FOUND.
- GET/PATCH /profile/allergies — allergènes/intolérances, liste d'IDs ;
  PATCH remplace l'ensemble complet (pas une fusion, cohérent avec un
  multi-select). ID invalide -> 404 ALLERGY_NOT_FOUND.
- 3 nouveaux ErrorCode (4041-4043) + libellés fr.
- Extraction de toSafeProfile() dans src/lib/safe-profile.ts —
  auparavant dupliqué dans auth.service.ts et require-auth.ts,
  profile.service.ts le réutilise aussi.
- Tests Mocha (28 passing) + Cucumber (15 scenarios) — même convention
  que le reste, doc README.

Deuxième commit de la feature profil/foyer/régime/allergènes —
composants front partagés dans le commit suivant.
2026-08-16 23:18:46 +02:00

83 lines
3.3 KiB
TypeScript

import { HttpError } from "@batch-cooking/error-tools";
import {
ErrorCode,
type LoginInput,
type SafeUserProfile,
type SignupInput,
} from "@batch-cooking/shared";
import argon2 from "argon2";
import { env } from "../../config/env.js";
import { prisma } from "../../db/prisma.js";
import { signAuthToken } from "../../lib/jwt.js";
import { toSafeProfile } from "../../lib/safe-profile.js";
/** Result of a successful signup/login: the safe profile plus the signed session JWT to set as a cookie. */
interface AuthResult {
/** The authenticated profile, safe to hand back to the client. */
profile: SafeUserProfile;
/** Signed session JWT — the caller sets this as the session cookie's value. */
token: string;
}
// argon2's defaults (64 MB memory, 3 passes) are deliberately expensive —
// that's the point, for real passwords. In tests we hash/verify dozens of
// times per run against throwaway data, so a much cheaper cost keeps the
// suite fast without weakening anything real. Never applies outside
// NODE_ENV=test.
const testHashOptions = { memoryCost: 8192, timeCost: 2, parallelism: 1 };
const hashOptions = env.NODE_ENV === "test" ? testHashOptions : undefined;
/**
* Creates a new household (`house`) and profile (`user_profiles`) together
* in one transaction, hashes the password, and issues a session token.
*
* @throws {HttpError} `409 EMAIL_ALREADY_IN_USE` if the email is already taken.
*/
export async function signup(input: SignupInput): Promise<AuthResult> {
const existing = await prisma.userProfile.findUnique({ where: { email: input.email } });
if (existing) {
throw new HttpError(409, ErrorCode.EMAIL_ALREADY_IN_USE, "Email already in use");
}
const passwordHash = await argon2.hash(input.password, hashOptions);
// A profile always belongs to a house; signup creates one, named after
// the new user for now — renamed via `PATCH /house/current` (the
// household step of the profile journey). Joining an existing house is a
// separate, not-yet-built feature.
const profile = await prisma.$transaction(async (tx) => {
const house = await tx.house.create({
data: { name: `Foyer de ${input.firstName}` },
});
return tx.userProfile.create({
data: {
firstName: input.firstName,
lastName: input.lastName,
email: input.email,
passwordHash,
houseId: house.id,
},
});
});
const token = signAuthToken({ userProfileId: profile.id, tokenVersion: profile.tokenVersion });
return { profile: toSafeProfile(profile), token };
}
/**
* Verifies credentials and issues a fresh session token.
*
* @throws {HttpError} `401 INVALID_CREDENTIALS` for either an unknown email
* or a wrong password — deliberately the same error either way, so a
* caller can never learn whether a given email has an account.
*/
export async function login(input: LoginInput): Promise<AuthResult> {
const profile = await prisma.userProfile.findUnique({ where: { email: input.email } });
if (!profile || !(await argon2.verify(profile.passwordHash, input.password))) {
throw new HttpError(401, ErrorCode.INVALID_CREDENTIALS, "Invalid email or password");
}
const token = signAuthToken({ userProfileId: profile.id, tokenVersion: profile.tokenVersion });
return { profile: toSafeProfile(profile), token };
}