validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string'], 'device_name' => ['nullable', 'string', 'max:120'], ]); $response = $this->identityPost('/api/identity/auth/login', [ 'email' => $credentials['email'], 'password' => $credentials['password'], ]); if ($response->status() === 422) { throw ValidationException::withMessages([ 'email' => ['The email or password is incorrect.'], ]); } $user = $this->provisionFromResponse($response); return $this->issueToken($user, $credentials['device_name'] ?? null); } public function register(Request $request): JsonResponse { // Mirror the web signup form; the monolith validates authoritatively, but // we pre-validate so the app gets fast, field-level feedback. $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'company' => ['nullable', 'string', 'max:255'], 'address' => ['required', 'string', 'max:255'], 'city' => ['required', 'string', 'max:255'], 'state' => ['required', 'string', 'max:255'], 'country' => ['required', 'string', 'size:2'], 'zipcode' => ['required', 'string', 'max:32'], 'phone_cc' => ['required', 'string', 'max:8'], 'phone' => ['required', 'string', 'max:32'], 'mobile_cc' => ['nullable', 'string', 'max:8'], 'mobile' => ['nullable', 'string', 'max:32'], 'terms' => ['accepted'], 'device_name' => ['nullable', 'string', 'max:120'], ]); $response = $this->identityPost('/api/identity/auth/register', array_merge( $request->only([ 'name', 'email', 'password', 'password_confirmation', 'company', 'address', 'city', 'state', 'country', 'zipcode', 'phone_cc', 'phone', 'mobile_cc', 'mobile', ]), ['terms' => $request->boolean('terms')], )); // Surface the monolith's validation errors (e.g. email already taken). if ($response->status() === 422) { throw ValidationException::withMessages( $response->json('errors') ?: ['email' => [$response->json('message') ?: 'Registration failed.']], ); } $user = $this->provisionFromResponse($response); return $this->issueToken($user, $data['device_name'] ?? null); } public function logout(Request $request): JsonResponse { $request->user()?->currentAccessToken()?->delete(); return response()->json(['data' => ['message' => 'Signed out.']]); } public function me(Request $request): JsonResponse { return response()->json(['data' => $this->presentUser($request->user())]); } private function identity(): \Illuminate\Http\Client\PendingRequest { return Http::baseUrl(rtrim((string) config('services.ladill_identity.url'), '/')) ->withToken((string) config('services.ladill_identity.key')) ->connectTimeout(10) ->timeout(20) ->acceptJson() ->asJson(); } /** POST to the identity API, turning connection failures into a clean message. */ private function identityPost(string $path, array $payload): HttpResponse { try { return $this->identity()->post($path, $payload); } catch (ConnectionException $e) { throw ValidationException::withMessages([ 'email' => ['Could not reach Ladill sign-in. Please try again in a moment.'], ]); } } /** Upsert the local mirror from the identity API's OIDC claims. */ private function provisionFromResponse(HttpResponse $response): User { if ($response->failed()) { throw ValidationException::withMessages([ 'email' => ['We could not reach Ladill sign-in. Please try again.'], ]); } $claims = (array) $response->json('data.user', []); $sub = (string) ($claims['sub'] ?? ''); if ($sub === '') { throw ValidationException::withMessages([ 'email' => ['We could not verify your Ladill account. Please try again.'], ]); } $email = (string) ($claims['email'] ?? ''); return User::updateOrCreate( ['public_id' => $sub], [ 'name' => $claims['name'] ?? null, 'email' => $email !== '' ? $email : $sub.'@users.ladill.com', 'avatar_url' => $claims['picture'] ?? null, ], ); } private function issueToken(User $user, ?string $deviceName): JsonResponse { QrTeamMember::linkPendingInvitesFor($user); $token = $user->createToken($deviceName ?: 'Ladill Mini Android', ['mini:read', 'mini:write']); return response()->json([ 'data' => [ 'token' => $token->plainTextToken, 'user' => $this->presentUser($user), ], ]); } /** @return array */ private function presentUser(User $user): array { // On the public login/register routes SetActingAccount hasn't run and // there's no authenticated guard yet, so ladill_account() is null — // the acting account at sign-in is simply the user themselves. $account = ladill_account() ?? $user; return [ 'id' => $user->id, 'public_id' => $user->public_id, 'name' => $user->name, 'email' => $user->email, 'avatar_url' => $user->avatarUrl(), 'acting_account' => [ 'id' => $account->id, 'public_id' => $account->public_id, 'name' => $account->name, 'email' => $account->email, ], 'accessible_account_ids' => $user->accessibleAccounts()->pluck('id')->values(), ]; } }