250 lines
8.5 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|