Files
ladill-mini/app/Http/Controllers/Api/AuthController.php
T
isaaccladandClaude Opus 4.8 cd56771b59
Deploy Ladill Mini / deploy (push) Successful in 31s
Mobile auth: native login + register via central identity API.
Login no longer uses the OAuth password grant (which wasn't enabled, the cause
of login failures). Both login and the new register endpoint proxy to
auth.ladill.com /api/identity/auth/* (shared IDENTITY_API_KEY_MINI), provision
the local mirror, and issue a Sanctum token — one Ladill identity across web
and app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:04:04 +00:00

178 lines
6.3 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\QrTeamMember;
use App\Models\User;
use Illuminate\Http\Client\Response as HttpResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
/**
* Native token auth for the Ladill Mini mobile app.
*
* Login and registration proxy to the central Ladill identity API
* (auth.ladill.com /api/identity/auth/*, gated by the shared first-party
* service key) so app accounts are the same single identity used by the website
* and every other Ladill app. We then provision the thin local user mirror
* (keyed by the OIDC `sub`, exactly like the web SSO callback) and hand back a
* Sanctum token the app uses for `auth:sanctum` requests.
*/
class AuthController extends Controller
{
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
'device_name' => ['nullable', 'string', 'max:120'],
]);
$response = $this->identity()->post('/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->identity()->post('/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'))
->acceptJson()
->asJson();
}
/** 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<string, mixed> */
private function presentUser(User $user): array
{
$account = ladill_account();
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(),
];
}
}