Files
alrahma_sunday_school_api/app/Http/Controllers/Api/Auth/AuthSessionController.php
T
2026-06-08 22:06:30 -04:00

250 lines
8.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginSessionRequest;
use App\Http\Requests\Auth\SetRoleSessionRequest;
use App\Models\User;
use App\Services\ApplicationUrlService;
use App\Services\Auth\ApiLoginSecurityService;
use App\Services\Auth\AuthSessionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
/**
* Session (cookie) endpoints for SPA — JSON alongside JWT on /api/login.
*
* Routes: GET login, POST user/login, GET logout, GET select-role, POST set-role (registered on web middleware).
*/
class AuthSessionController extends Controller
{
public function __construct(
private ApiLoginSecurityService $security,
private AuthSessionService $sessionAuth,
private ApplicationUrlService $urls,
) {}
/** Returns login bootstrap state for the web portal. */
public function loginMask(Request $request): JsonResponse
{
$redirectTo = $this->sessionAuth->sanitizeRedirectTarget(
(string) ($request->query('redirect_to') ?? ''),
);
if (! Auth::guard('web')->check()) {
return response()->json([
'status' => true,
'authenticated' => false,
'redirect_to' => $redirectTo,
]);
}
/** @var list<string>|null $roles */
$roles = session('roles');
$roles = is_array($roles) ? array_values(array_filter(array_map('strval', $roles))) : [];
$currentRole = session('role');
if ($roles !== [] && count($roles) > 1 && ! $currentRole) {
return response()->json([
'status' => true,
'authenticated' => true,
'next_url' => $this->urls->webSelectRoleUrl(false),
'roles' => $roles,
'pending_redirect' => session('post_login_redirect'),
]);
}
if ($redirectTo !== null) {
return response()->json([
'status' => true,
'authenticated' => true,
'next_url' => $redirectTo,
]);
}
$pick = $currentRole !== null && $currentRole !== ''
? [(string) $currentRole]
: $roles;
return response()->json([
'status' => true,
'authenticated' => true,
'next_url' => $this->sessionAuth->dashboardRouteForRoles($pick),
]);
}
/** Creates a web session from the submitted credentials. */
public function login(LoginSessionRequest $request): JsonResponse
{
$email = (string) $request->validated('email');
$password = (string) $request->validated('password');
$redirectRaw = $request->validated('redirect_to');
$sanitizedRedirect = $redirectRaw !== null
? $this->sessionAuth->sanitizeRedirectTarget((string) $redirectRaw)
: null;
$ip = (string) $request->ip();
if ($this->security->isIpBlocked($ip)) {
return response()->json([
'status' => false,
'message' => 'Too many failed attempts from your IP. Please try again later.',
], 429);
}
$user = User::query()->where('email', $email)->first();
if (! $user) {
$this->security->logIpAttempt($ip);
return response()->json([
'status' => false,
'message' => 'The email and password combination you entered is invalid. Please try again.',
], 401);
}
if ($user->is_suspended) {
return response()->json([
'status' => false,
'message' => 'Account suspended. Please check your email to reset your password.',
], 403);
}
if (! verify_stored_password($password, (string) $user->password)) {
$this->security->handleFailedLogin($user, $email, $ip);
return response()->json([
'status' => false,
'message' => 'The email and password combination you entered is invalid. Please try again.',
], 401);
}
$fresh = $user->fresh();
if (! $fresh) {
return response()->json([
'status' => false,
'message' => 'The email and password combination you entered is invalid. Please try again.',
], 401);
}
$this->security->resetFailedAttempts($fresh);
$this->security->logSuccessfulLogin($fresh, $request);
$dest = $this->sessionAuth->resolvePostLoginDestination($fresh, $sanitizedRedirect);
if (($dest['kind'] ?? '') === 'no_roles') {
return response()->json([
'status' => false,
'message' => (string) ($dest['reason'] ?? 'Role not assigned. Please contact support.'),
], 403);
}
$loginPayload = $this->security->buildLoginResponse($fresh, JWTAuth::fromUser($fresh));
return response()->json(array_merge($loginPayload, [
'requires_role_selection' => ($dest['kind'] ?? '') === 'select_role',
'roles' => $dest['roles'] ?? null,
'next_url' => $dest['redirect_url'] ?? $this->urls->docsHomeUrl(false),
]));
}
/** Closes the current web session. */
public function logout(Request $request): JsonResponse
{
$userId = Auth::guard('web')->id();
$email = Auth::guard('web')->user()?->email ?? session('user_email');
$this->sessionAuth->logLogout($userId ? (int) $userId : null, $email ? (string) $email : null);
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'status' => true,
'next_url' => $this->urls->docsHomeUrl(false),
]);
}
/** Returns the available post-login roles for the current user. */
public function selectRole(Request $request): JsonResponse
{
if (! Auth::guard('web')->check()) {
return response()->json([
'status' => false,
'message' => 'No roles available.',
], 401);
}
/** @var list<string>|null $roles */
$roles = session('roles');
$roles = is_array($roles) ? array_values(array_filter(array_map('strval', $roles))) : [];
if ($roles === []) {
return response()->json([
'status' => false,
'message' => 'No roles available.',
], 422);
}
return response()->json([
'status' => true,
'roles' => $roles,
'pending_redirect' => session('post_login_redirect'),
'redirect_to_query' => $this->sessionAuth->sanitizeRedirectTarget(
(string) ($request->query('redirect_to') ?? ''),
),
]);
}
/** Stores the selected post-login role in session state. */
public function setRole(SetRoleSessionRequest $request): JsonResponse
{
if (! Auth::guard('web')->check()) {
return response()->json([
'status' => false,
'message' => 'Unauthorized.',
], 401);
}
$selectedRole = (string) $request->validated('selected_role');
/** @var list<string>|null $available */
$available = session('roles');
$available = is_array($available) ? array_values(array_filter(array_map('strval', $available))) : [];
if ($available === [] || ! in_array($selectedRole, $available, true)) {
return response()->json([
'status' => false,
'message' => 'Invalid role selected.',
], 422);
}
$redirectRaw = $request->validated('redirect_to');
$redirectTo = $redirectRaw !== null
? $this->sessionAuth->sanitizeRedirectTarget((string) $redirectRaw)
: null;
if ($redirectTo === null) {
$redirectTo = $this->sessionAuth->sanitizeRedirectTarget(
(string) (session('post_login_redirect') ?? ''),
);
}
session(['role' => $selectedRole]);
session()->forget('post_login_redirect');
$next = $redirectTo ?? $this->sessionAuth->dashboardRouteForRoles([$selectedRole]);
return response()->json([
'status' => true,
'role' => $selectedRole,
'next_url' => $next,
]);
}
}