credentials($request); if ($this->users->findByEmail($email) !== null) { throw new ApiException('That email address is already registered.', 409); } $user = $this->users->create($email, password_hash($password, PASSWORD_DEFAULT)); return $this->json($response, $this->session($user), 201); } /** * POST /api/auth/login */ public function login(Request $request, Response $response): Response { [$email, $password] = $this->credentials($request); $user = $this->users->findByEmail($email); if ($user === null || !password_verify($password, $user['password_hash'])) { // Same message either way so we don't reveal which emails are registered. throw new ApiException('Invalid email or password.', 401); } return $this->json($response, $this->session($user)); } /** * GET /api/me (requires AuthMiddleware) */ public function me(Request $request, Response $response): Response { /** @var array{id: int, email: string, email_verified_at: string|null, created_at: string} $user */ $user = $request->getAttribute('user'); return $this->json($response, ['user' => $this->presentUser($user)]); } /** * Extract and validate the email/password pair from the request body. * * @return array{0: string, 1: string} Normalised email and raw password. */ private function credentials(Request $request): array { $body = (array) ($request->getParsedBody() ?? []); $email = is_string($body['email'] ?? null) ? trim($body['email']) : ''; $password = is_string($body['password'] ?? null) ? $body['password'] : ''; $errors = []; if ($email === '') { $errors['email'][] = 'Email is required.'; } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors['email'][] = 'Email must be a valid address.'; } elseif (strlen($email) > self::EMAIL_MAX) { $errors['email'][] = sprintf('Email must be at most %d characters.', self::EMAIL_MAX); } if ($password === '') { $errors['password'][] = 'Password is required.'; } elseif (strlen($password) < self::PASSWORD_MIN) { $errors['password'][] = sprintf('Password must be at least %d characters.', self::PASSWORD_MIN); } elseif (strlen($password) > self::PASSWORD_MAX) { $errors['password'][] = sprintf('Password must be at most %d characters.', self::PASSWORD_MAX); } if ($errors !== []) { throw new ValidationException($errors); } return [mb_strtolower($email), $password]; } /** * Build the standard authentication payload returned by register and login. * * @param array{id: int, email: string, email_verified_at: string|null, created_at: string} $user * @return array */ private function session(array $user): array { $token = $this->jwt->issue($user); return [ 'user' => $this->presentUser($user), 'token' => $token['token'], 'expires_at' => $token['expires_at'], ]; } /** * @param array{id: int, email: string, email_verified_at?: string|null, created_at?: string} $user * @return array */ private function presentUser(array $user): array { $verifiedAt = $user['email_verified_at'] ?? null; return [ 'id' => (int) $user['id'], 'email' => $user['email'], 'email_verified' => $verifiedAt !== null, 'email_verified_at' => $verifiedAt, 'created_at' => $user['created_at'] ?? null, ]; } }