Deploy Ladill Mini / deploy (push) Successful in 31s
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>
178 lines
6.3 KiB
PHP
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(),
|
|
];
|
|
}
|
|
}
|