343 lines
8.9 KiB
PHP
343 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Auth\LoginRequest;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
|
|
class AuthenticatedSessionController extends Controller
|
|
{
|
|
/**
|
|
* Show the login page.
|
|
*/
|
|
public function create(Request $request): Response
|
|
{
|
|
return Inertia::render('auth/login', [
|
|
'canResetPassword' => Route::has('password.request'),
|
|
'status' => $request->session()->get('status'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle an incoming authentication request.
|
|
*/
|
|
public function store(LoginRequest $request): SymfonyResponse
|
|
{
|
|
try {
|
|
$request->authenticate();
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
$request->session()->flash('error', __('auth.login_failed'));
|
|
|
|
return redirect()->route('login')->withErrors($e->errors());
|
|
}
|
|
|
|
Log::info('Login attempt', ['login' => $request->login, 'authenticated' => Auth::check()]);
|
|
|
|
$request->session()->regenerate();
|
|
$request->session()->flash('success', __('auth.login_success'));
|
|
|
|
$user = Auth::user();
|
|
if ($user && $user->email_verified_at === null) {
|
|
$intended = $request->session()->get('url.intended');
|
|
$intended = is_string($intended) ? trim($intended) : null;
|
|
|
|
if ($this->isVerificationLink($intended)) {
|
|
$request->session()->forget('url.intended');
|
|
|
|
return Inertia::location($intended);
|
|
}
|
|
|
|
return Inertia::location(route('verification.notice'));
|
|
}
|
|
|
|
$intended = $this->resolveIntended($request);
|
|
if ($intended !== null) {
|
|
$this->rememberTenantAdminTarget($request, $intended);
|
|
|
|
return Inertia::location($intended);
|
|
}
|
|
|
|
$returnTo = $this->resolveReturnTo($request);
|
|
if ($returnTo !== null) {
|
|
$this->rememberTenantAdminTarget($request, $returnTo);
|
|
|
|
return Inertia::location($returnTo);
|
|
}
|
|
|
|
$default = $this->defaultAdminPath();
|
|
$this->rememberTenantAdminTarget($request, $default);
|
|
|
|
return Inertia::location($default);
|
|
}
|
|
|
|
/**
|
|
* Destroy an authenticated session.
|
|
*/
|
|
public function destroy(Request $request): RedirectResponse
|
|
{
|
|
Auth::guard('web')->logout();
|
|
|
|
$request->session()->invalidate();
|
|
$request->session()->regenerateToken();
|
|
|
|
return redirect('/');
|
|
}
|
|
|
|
private function resolveReturnTo(Request $request): ?string
|
|
{
|
|
$encoded = $request->string('return_to')->trim();
|
|
|
|
if ($encoded === '') {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeTenantAdminTarget(
|
|
$this->decodeReturnTo($encoded, $request),
|
|
$request
|
|
);
|
|
}
|
|
|
|
private function resolveIntended(Request $request): ?string
|
|
{
|
|
$intended = $request->session()->pull('url.intended');
|
|
|
|
if (! is_string($intended)) {
|
|
return null;
|
|
}
|
|
|
|
$trimmed = trim($intended);
|
|
if ($trimmed === '') {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeTenantAdminTarget(
|
|
$this->decodeReturnTo($trimmed, $request),
|
|
$request
|
|
);
|
|
}
|
|
|
|
private function isVerificationLink(?string $target): bool
|
|
{
|
|
if (! is_string($target) || trim($target) === '') {
|
|
return false;
|
|
}
|
|
|
|
$path = trim($target);
|
|
|
|
if (str_starts_with($path, '/verify-email/')) {
|
|
return true;
|
|
}
|
|
|
|
$parsed = parse_url($path);
|
|
|
|
if ($parsed === false) {
|
|
return false;
|
|
}
|
|
|
|
$path = $parsed['path'] ?? '';
|
|
|
|
return $path !== '' && str_starts_with($path, '/verify-email/');
|
|
}
|
|
|
|
private function decodeReturnTo(string $value, Request $request): ?string
|
|
{
|
|
$candidate = $this->decodeBase64Url($value) ?? $value;
|
|
$candidate = trim($candidate);
|
|
|
|
if ($candidate === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_starts_with($candidate, '//')) {
|
|
return null;
|
|
}
|
|
|
|
if (str_starts_with($candidate, '/')) {
|
|
return $candidate;
|
|
}
|
|
|
|
$targetHost = parse_url($candidate, PHP_URL_HOST);
|
|
$scheme = parse_url($candidate, PHP_URL_SCHEME);
|
|
|
|
if (! $scheme || ! $targetHost) {
|
|
return null;
|
|
}
|
|
|
|
$appHost = parse_url($request->getSchemeAndHttpHost(), PHP_URL_HOST);
|
|
|
|
if (! $appHost || ! $this->isAllowedReturnHost($targetHost, $appHost)) {
|
|
return null;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function defaultAdminPath(): string
|
|
{
|
|
$user = Auth::user();
|
|
|
|
// Block users with 'user' role - redirect to package selection
|
|
if ($user && $user->role === 'user') {
|
|
return '/packages';
|
|
}
|
|
|
|
// Super admins go to Filament superadmin panel
|
|
if ($user && $user->isSuperAdmin()) {
|
|
return '/super-admin';
|
|
}
|
|
|
|
// Tenant admins go to their PWA dashboard
|
|
if ($user && $user->role === 'tenant_admin') {
|
|
return '/event-admin/dashboard';
|
|
}
|
|
|
|
// Fallback: redirect to packages (for users with no role)
|
|
return '/packages';
|
|
}
|
|
|
|
private function normalizeTenantAdminTarget(?string $target, Request $request): ?string
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (! $user || $user->role !== 'tenant_admin') {
|
|
return $target;
|
|
}
|
|
|
|
if ($target === null || $target === '') {
|
|
return '/event-admin/dashboard';
|
|
}
|
|
|
|
$parsed = parse_url($target);
|
|
$path = $target;
|
|
$hasScheme = false;
|
|
|
|
if ($parsed !== false) {
|
|
$hasScheme = isset($parsed['scheme']);
|
|
$host = $parsed['host'] ?? null;
|
|
$scheme = $parsed['scheme'] ?? null;
|
|
$requestHost = parse_url($request->getSchemeAndHttpHost(), PHP_URL_HOST);
|
|
|
|
if ($scheme && $host && $requestHost && ! $this->isAllowedReturnHost($host, $requestHost)) {
|
|
return '/event-admin/dashboard';
|
|
}
|
|
|
|
if (isset($parsed['path'])) {
|
|
$path = $parsed['path'];
|
|
if (isset($parsed['query'])) {
|
|
$path .= '?'.$parsed['query'];
|
|
}
|
|
if (isset($parsed['fragment'])) {
|
|
$path .= '#'.$parsed['fragment'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! str_starts_with($path, '/')) {
|
|
$path = '/'.$path;
|
|
}
|
|
|
|
if (str_starts_with($path, '/event-admin')) {
|
|
return $hasScheme ? $target : $path;
|
|
}
|
|
|
|
return '/event-admin/dashboard';
|
|
}
|
|
|
|
private function decodeBase64Url(string $value): ?string
|
|
{
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
$padded = str_pad($value, strlen($value) + ((4 - (strlen($value) % 4)) % 4), '=');
|
|
$normalized = strtr($padded, '-_', '+/');
|
|
$decoded = base64_decode($normalized, true);
|
|
|
|
if ($decoded === false) {
|
|
return null;
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
private function isAllowedReturnHost(string $targetHost, string $appHost): bool
|
|
{
|
|
if ($targetHost === $appHost) {
|
|
return true;
|
|
}
|
|
|
|
return Str::endsWith($targetHost, '.'.$appHost);
|
|
}
|
|
|
|
private function rememberTenantAdminTarget(Request $request, ?string $target): void
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (! $user || $user->role !== 'tenant_admin') {
|
|
return;
|
|
}
|
|
|
|
if (! is_string($target) || $target === '') {
|
|
return;
|
|
}
|
|
|
|
$normalized = $this->normalizeTenantAdminTarget($target, $request);
|
|
|
|
if (! is_string($normalized) || $normalized === '') {
|
|
return;
|
|
}
|
|
|
|
$path = $this->extractTenantAdminPath($normalized);
|
|
|
|
if ($path === null) {
|
|
return;
|
|
}
|
|
|
|
$request->session()->put('tenant_admin.return_to', $path);
|
|
}
|
|
|
|
private function extractTenantAdminPath(string $target): ?string
|
|
{
|
|
$value = trim($target);
|
|
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_starts_with($value, '/event-admin')) {
|
|
return $value;
|
|
}
|
|
|
|
$parsed = parse_url($value);
|
|
|
|
if ($parsed === false) {
|
|
return null;
|
|
}
|
|
|
|
$path = $parsed['path'] ?? '';
|
|
|
|
if ($path === '' || ! str_starts_with($path, '/event-admin')) {
|
|
return null;
|
|
}
|
|
|
|
if (isset($parsed['query'])) {
|
|
$path .= '?'.$parsed['query'];
|
|
}
|
|
|
|
if (isset($parsed['fragment'])) {
|
|
$path .= '#'.$parsed['fragment'];
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
}
|