validated(); $user = User::query() ->when($data['email'] ?? null, fn ($q, $email) => $q->where('email', $email)) ->when($data['phone'] ?? null, fn ($q, $phone) => $q->where('phone', $phone)) ->first(); if (! $user || ! Hash::check($data['password'], $user->password)) { return $this->fail('Invalid credentials', null, 401); } if ($user->status === User::STATUS_SUSPENDED) { return $this->forbidden('Account suspended'); } if ($user->status === User::STATUS_PENDING) { return $this->fail( 'Account pending verification. Verify the OTP sent during registration.', ['account' => ['Verify your phone before logging in.']], 403, ); } // Tenant gate. Super admins bypass; everyone else must be logging // into their own LGU. Prevents cross-tenant credential reuse. if (! $user->isSuperAdmin()) { $tenant = Tenancy::current(); if (! $tenant) { return $this->fail( 'Pick your LGU first. Send X-Tenant-Code header.', ['tenant' => ['LGU code required']], 400, ); } if ((int) $user->tenant_id !== (int) $tenant->id) { return $this->fail('This account is not registered with the selected LGU.', null, 403); } } $user->forceFill(['last_login_at' => now()])->save(); $deviceName = $data['device_name'] ?? $request->userAgent() ?? 'unknown'; $token = $user->createToken($deviceName); return $this->ok([ 'user' => new UserResource($user), 'token' => $token->plainTextToken, 'token_type' => 'Bearer', ], 'Login successful'); } }