diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 744dd76..41be386 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -5,19 +5,21 @@ 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; /** - * Token login for the Ladill Mini mobile app. + * Native token auth for the Ladill Mini mobile app. * - * Mirrors the web "Sign in with Ladill" flow (SsoLoginController) but uses the - * OAuth Resource Owner Password grant so the native app can collect the - * credentials directly. On success we provision the same thin local user - * mirror keyed by the OIDC `sub` and hand back a Sanctum personal access token - * the app stores for `auth:sanctum` requests. + * 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 { @@ -29,48 +31,68 @@ class AuthController extends Controller 'device_name' => ['nullable', 'string', 'max:120'], ]); - $issuer = rtrim((string) config('services.ladill_sso.issuer'), '/'); - - $tokenRes = Http::asForm()->post($issuer.'/oauth/token', [ - 'grant_type' => 'password', - 'client_id' => (string) config('services.ladill_sso.client_id'), - 'client_secret' => (string) config('services.ladill_sso.client_secret'), - 'username' => $credentials['email'], + $response = $this->identity()->post('/api/identity/auth/login', [ + 'email' => $credentials['email'], 'password' => $credentials['password'], - 'scope' => 'openid profile email', ]); - if ($tokenRes->failed()) { + if ($response->status() === 422) { throw ValidationException::withMessages([ 'email' => ['The email or password is incorrect.'], ]); } - $user = $this->provisionFromAccessToken($issuer, (string) $tokenRes->json('access_token')); + $user = $this->provisionFromResponse($response); - if (! $user) { - throw ValidationException::withMessages([ - 'email' => ['We could not verify your Ladill account. Please try again.'], - ]); + 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.']], + ); } - QrTeamMember::linkPendingInvitesFor($user); + $user = $this->provisionFromResponse($response); - $deviceName = $credentials['device_name'] ?? 'Ladill Mini Android'; - $token = $user->createToken($deviceName, ['mini:read', 'mini:write']); - - return response()->json([ - 'data' => [ - 'token' => $token->plainTextToken, - 'user' => $this->presentUser($user), - ], - ]); + return $this->issueToken($user, $data['device_name'] ?? null); } public function logout(Request $request): JsonResponse { - $token = $request->user()?->currentAccessToken(); - $token?->delete(); + $request->user()?->currentAccessToken()?->delete(); return response()->json(['data' => ['message' => 'Signed out.']]); } @@ -80,30 +102,58 @@ class AuthController extends Controller return response()->json(['data' => $this->presentUser($request->user())]); } - private function provisionFromAccessToken(string $issuer, string $accessToken): ?User + private function identity(): \Illuminate\Http\Client\PendingRequest { - if ($accessToken === '') { - return null; + 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 = Http::withToken($accessToken)->acceptJson()->get($issuer.'/oauth/userinfo'); + $claims = (array) $response->json('data.user', []); + $sub = (string) ($claims['sub'] ?? ''); - if ($claims->failed() || ! $claims->json('sub')) { - return null; + if ($sub === '') { + throw ValidationException::withMessages([ + 'email' => ['We could not verify your Ladill account. Please try again.'], + ]); } - $email = (string) ($claims->json('email') ?: ''); + $email = (string) ($claims['email'] ?? ''); return User::updateOrCreate( - ['public_id' => (string) $claims->json('sub')], + ['public_id' => $sub], [ - 'name' => $claims->json('name'), - 'email' => $email !== '' ? $email : (string) $claims->json('sub').'@users.ladill.com', - 'avatar_url' => $claims->json('picture'), + '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 { diff --git a/config/services.php b/config/services.php index dddac94..2f3ab98 100644 --- a/config/services.php +++ b/config/services.php @@ -26,6 +26,14 @@ return [ 'redirect' => rtrim((string) env('APP_URL', 'https://mini.ladill.com'), '/').'/sso/callback', ], + // Central Ladill identity API (auth.ladill.com /api/identity/auth/*). The + // mobile app's native login/register proxy through here, gated by the + // shared first-party service key (config/identity.php on the monolith). + 'ladill_identity' => [ + 'url' => 'https://'.config('app.auth_domain'), + 'key' => env('IDENTITY_API_KEY_MINI'), + ], + 'ladill_webmail' => [ 'url' => env('LADILL_WEBMAIL_URL', 'https://mail.ladill.com'), ], diff --git a/routes/api.php b/routes/api.php index 3e9827c..8b6d354 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,8 +10,10 @@ use App\Http\Controllers\Api\QrCodeController; use Illuminate\Support\Facades\Route; Route::prefix('v1')->group(function () { - // Mobile token login (Ladill Mini Android). Public — issues a Sanctum token. - Route::post('/auth/login', [AuthController::class, 'login'])->name('api.auth.login'); + // Mobile token auth (Ladill Mini Android). Public — proxies to the central + // Ladill identity API and issues a Sanctum token. + Route::post('/auth/login', [AuthController::class, 'login'])->middleware('throttle:10,1')->name('api.auth.login'); + Route::post('/auth/register', [AuthController::class, 'register'])->middleware('throttle:10,1')->name('api.auth.register'); Route::middleware(['auth:sanctum', \App\Http\Middleware\SetActingAccount::class])->group(function () { Route::post('/auth/logout', [AuthController::class, 'logout'])->name('api.auth.logout');