import { z } from "zod"; import type { EventBusProtocol, ConsentFactoryProtocol, } from "@repo/core-shared/di"; import { userSignedUpEvent } from "../../events/user-signed-up.event"; import { AuthenticationError } from "../../entities/errors/auth"; import { cookieSchema } from "../../entities/models/cookie"; import { sessionSchema } from "../../entities/models/session"; import type { IUsersRepository } from "../repositories/users.repository.interface"; import type { IAuthenticationService } from "../services/authentication.service.interface"; // Cookie name written by the anonymous consent banner (mirrors CONSENT_COOKIE_NAME in @repo/core-consent). const ANONYMOUS_CONSENT_COOKIE = "cc_consent"; function extractConsentFromCookieHeader(cookieHeader: string): string[] | null { for (const part of cookieHeader.split(";")) { const eqIdx = part.indexOf("="); if (eqIdx === -1) continue; const name = part.slice(0, eqIdx).trim(); if (name !== ANONYMOUS_CONSENT_COOKIE) continue; const value = part.slice(eqIdx + 1).trim(); const cats = value .split(",") .map((c) => c.trim()) .filter(Boolean); return cats.length > 0 ? cats : null; } return null; } // ── Input ──────────────────────────────────────────────────────────────── export const signUpInputSchema = z .object({ username: z.string().min(3).max(31), password: z.string().min(6).max(255), confirmPassword: z.string().min(6).max(255), cookieHeader: z.string().optional(), }) .strict() .refine((d) => d.password === d.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"], }); export type SignUpInput = z.infer; // ── Output ─────────────────────────────────────────────────────────────── export const signUpOutputSchema = z.object({ session: sessionSchema, cookie: cookieSchema, clearCookie: cookieSchema.optional(), }); export type SignUpOutput = z.infer; // ── Use case ───────────────────────────────────────────────────────────── export type ISignUpUseCase = ReturnType; export const signUpUseCase = ( usersRepository: IUsersRepository, authenticationService: IAuthenticationService, bus: EventBusProtocol | undefined, consentFactory: ConsentFactoryProtocol | undefined, ) => async (input: SignUpInput): Promise => { const existingUser = await usersRepository.getUserByUsername( input.username, ); if (existingUser) { throw new AuthenticationError("Username taken"); } const passwordHash = await authenticationService.hashPassword( input.password, ); const userId = authenticationService.generateUserId(); const newUser = await usersRepository.createUser({ id: userId, username: input.username, passwordHash, }); const { cookie, session } = await authenticationService.createSession(newUser); // Auth is username-based — synthesize a deterministic email so the event // payload validates against userSignedUpEventSchema.email(). // bus is optional: absent when core-events is not wired. if (bus) { await bus.publish(userSignedUpEvent, { userId: newUser.id, email: `${newUser.username}@example.local`, signedUpAt: new Date().toISOString(), }); } // Migrate anonymous consent when both a cookie header and a consent factory // are present. consentFactory is optional: absent when core-consent is not wired. const cookieState = input.cookieHeader ? extractConsentFromCookieHeader(input.cookieHeader) : null; let clearCookie: z.infer | undefined; if (cookieState && consentFactory) { const consent = await consentFactory(newUser.id); for (const category of cookieState) { await consent.grant(category, { method: "signup-migration" }); } clearCookie = { name: ANONYMOUS_CONSENT_COOKIE, value: "", attributes: { maxAge: 0, path: "/" }, }; } return signUpOutputSchema.parse({ session, cookie, clearCookie }); };