Add mobile API for Ladill Mini Android app.
Deploy Ladill Mini / deploy (push) Successful in 47s

Token login via OAuth password grant against auth.ladill.com (AuthController),
plus /api/v1/mini endpoints (overview, payment-qrs CRUD + preview, payments,
payouts) mirroring the web Mini controllers. Sanctum abilities mini:read/write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
isaacclad
2026-06-10 22:13:30 +00:00
co-authored by Claude Opus 4.8
parent 70ded0e668
commit 307248749b
7 changed files with 501 additions and 7 deletions
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\QrTeamMember;
use App\Models\User;
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.
*
* 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.
*/
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'],
]);
$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'],
'password' => $credentials['password'],
'scope' => 'openid profile email',
]);
if ($tokenRes->failed()) {
throw ValidationException::withMessages([
'email' => ['The email or password is incorrect.'],
]);
}
$user = $this->provisionFromAccessToken($issuer, (string) $tokenRes->json('access_token'));
if (! $user) {
throw ValidationException::withMessages([
'email' => ['We could not verify your Ladill account. Please try again.'],
]);
}
QrTeamMember::linkPendingInvitesFor($user);
$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),
],
]);
}
public function logout(Request $request): JsonResponse
{
$token = $request->user()?->currentAccessToken();
$token?->delete();
return response()->json(['data' => ['message' => 'Signed out.']]);
}
public function me(Request $request): JsonResponse
{
return response()->json(['data' => $this->presentUser($request->user())]);
}
private function provisionFromAccessToken(string $issuer, string $accessToken): ?User
{
if ($accessToken === '') {
return null;
}
$claims = Http::withToken($accessToken)->acceptJson()->get($issuer.'/oauth/userinfo');
if ($claims->failed() || ! $claims->json('sub')) {
return null;
}
$email = (string) ($claims->json('email') ?: '');
return User::updateOrCreate(
['public_id' => (string) $claims->json('sub')],
[
'name' => $claims->json('name'),
'email' => $email !== '' ? $email : (string) $claims->json('sub').'@users.ladill.com',
'avatar_url' => $claims->json('picture'),
],
);
}
/** @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(),
];
}
}