import { z } from "zod"; import type { IRateLimit } from "@repo/core-shared/rate-limit"; import { AuthenticationError, TooManyRequestsError, } 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"; // ── Input ──────────────────────────────────────────────────────────────── // `.strict()` + no clientIp field: a client submitting clientIp is rejected // at the procedure boundary (audit finding B2). export const signInInputSchema = z .object({ username: z.string().min(3).max(31), password: z.string().min(6).max(255), }) .strict(); export type SignInInput = z.infer; /** * Server-derived per-request context, typed OUTSIDE the public input schema * so it can never be client-supplied (audit finding B2). The tRPC adapter * derives `clientIp` from trusted proxy headers and the controller threads * it through; `undefined` means "no proxy header present" and falls into a * shared bucket. */ export type SignInRequestContext = { clientIp?: string; }; // ── Output ─────────────────────────────────────────────────────────────── export const signInOutputSchema = z.object({ session: sessionSchema, cookie: cookieSchema, }); export type SignInOutput = z.infer; // ── Use case ───────────────────────────────────────────────────────────── export type ISignInUseCase = ReturnType; export const signInUseCase = ( usersRepository: IUsersRepository, authenticationService: IAuthenticationService, rateLimit: IRateLimit, ) => async (input: SignInInput & SignInRequestContext): Promise => { const { allowed: ipAllowed } = await rateLimit.consume( "ip", `signIn:ip:${input.clientIp ?? ""}`, ); if (!ipAllowed) throw new TooManyRequestsError("Too many sign-in attempts"); const { allowed: accountAllowed } = await rateLimit.consume( "account", `signIn:account:${input.username}`, ); if (!accountAllowed) throw new TooManyRequestsError("Too many sign-in attempts"); const existingUser = await usersRepository.getUserByUsername( input.username, ); if (!existingUser) { throw new AuthenticationError("User does not exist"); } const validPassword = await authenticationService.verifyPassword( existingUser.passwordHash, input.password, ); if (!validPassword) { throw new AuthenticationError("Incorrect username or password"); } const result = await authenticationService.createSession(existingUser); return signInOutputSchema.parse(result); };