Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
<?php
// app/Http/Middleware/AuthAnyGuard.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class AuthAnyGuard
{
public function handle($request, Closure $next, ...$guards)
{
// List of route names or paths to skip
$excludedRoutes = [
'login',
'admin.login',
'bank.login',
// add other public route names here
];
if ($request->route() && in_array($request->route()->getName(), $excludedRoutes)) {
return $next($request);
}
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
Auth::shouldUse($guard);
return $next($request);
}
}
// Store the intended URL before redirecting to login
return redirect()->guest(LaravelLocalization::localizeUrl('/login'));
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return LaravelLocalization::localizeUrl('/login');
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthenticateAdmin
{
/**
* Get the path the admin should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
public function handle(Request $request, Closure $next)
{
if (!Auth::guard('admin')->check()) {
// Get the active profile ID and find its index in the user's profiles collection
if (session('activeProfileId')) {
// Find the position/index of this profile in the user's profile collection
$user = Auth::guard('web')->user();
$userWithRelations = User::with(['organizations', 'banksManaged', 'admins'])->find($user->id);
$profiles = $userWithRelations->organizations
->merge($userWithRelations->banksManaged)
->merge($userWithRelations->admins);
// Find the index of the profile with this ID
$activeProfileId = session('activeProfileId');
$index = $profiles->search(function($item) use ($activeProfileId) {
return $item->id == $activeProfileId && get_class($item) == 'App\Models\Admin';
});
// Store the index if found
if ($index !== false) {
session(['intended_profile_switch' => $index]);
}
}
// Clear any intended URL to prevent redirect loops after profile auth
$request->session()->forget('url.intended');
return redirect()->route('admin.login');
}
if (session('activeProfileType') !== 'App\Models\Admin') {
// Same logic as above
if (session('activeProfileId')) {
// Find the position/index of this profile in the user's profile collection
$user = Auth::guard('web')->user();
$userWithRelations = User::with(['organizations', 'banksManaged', 'admins'])->find($user->id);
$profiles = $userWithRelations->organizations
->merge($userWithRelations->banksManaged)
->merge($userWithRelations->admins);
// Find the index of the profile with this ID
$activeProfileId = session('activeProfileId');
$index = $profiles->search(function($item) use ($activeProfileId) {
return $item->id == $activeProfileId && get_class($item) == 'App\Models\Admin';
});
// Store the index if found
if ($index !== false) {
session(['intended_profile_switch' => $index]);
}
}
// Clear any intended URL to prevent redirect loops after profile auth
$request->session()->forget('url.intended');
return redirect()->route('admin.login');
}
return $next($request);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
class AuthenticateBank
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Instead of checking the guard, check only the session
if (session('activeProfileType') !== 'App\Models\Bank') {
// Get the active profile ID and find its index in the user's profiles collection
if (session('activeProfileId')) {
// Code to find the correct index for the bank login form
$user = Auth::guard('web')->user();
$userWithRelations = User::with(['organizations', 'banksManaged', 'admins'])->find($user->id);
$profiles = $userWithRelations->organizations
->merge($userWithRelations->banksManaged)
->merge($userWithRelations->admins);
// Find the index of the profile with this ID
$activeProfileId = session('activeProfileId');
$index = $profiles->search(function($item) use ($activeProfileId) {
return $item->id == $activeProfileId && get_class($item) == 'App\Models\Bank';
});
// Store the index if found
if ($index !== false) {
session(['intended_profile_switch' => $index]);
}
}
// Clear any intended URL to prevent redirect loops after profile auth
$request->session()->forget('url.intended');
return redirect()->route('bank.login');
}
return $next($request);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class CanOnWebGuard
{
public function handle($request, Closure $next, $permission)
{
$user = Auth::guard('web')->user();
// Use can() instead of hasPermissionTo() to work with Gate definitions
// can() is more flexible and works with the multi-guard permission system
if (!$user || !$user->can($permission)) {
abort(403, 'Unauthorized action.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config; // Import Config facade
use Illuminate\Support\Facades\Session;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class CheckProfileInactivity
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
// Only proceed if a user is authenticated
if (!Auth::check()) {
Session::forget('last_activity'); // Clear timestamp if user logs out
return $next($request); // Pass the request to the next middleware - route-level auth middleware will handle redirect
}
// When user is authenticated
$activeProfileType = Session::get('activeProfileType', \App\Models\User::class); // Default to User class if not set
// Initialize active profile if not set (happens after login)
if (!Session::has('activeProfileId')) {
$user = Auth::guard('web')->user();
if ($user) {
Session::put([
'activeProfileType' => \App\Models\User::class,
'activeProfileId' => $user->id,
'activeProfileName' => $user->name,
'activeProfilePhoto' => $user->profile_photo_path,
]);
}
}
$lastActivity = Session::get('last_activity');
// If last_activity is not set, initialize it and continue (first request after login)
if (!$lastActivity) {
Session::put('last_activity', now());
return $next($request);
}
// Check base session timeout first (this should never exceed SESSION_LIFETIME)
// This prevents profile-specific timeouts from keeping sessions alive beyond the base session lifetime
$baseSessionLifetime = Config::get('session.lifetime', 120); // minutes
$minutesSinceActivity = now()->diffInMinutes($lastActivity);
// If base session has expired, force logout regardless of profile type
if ($minutesSinceActivity >= $baseSessionLifetime) {
$user = Auth::guard('web')->user();
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
// For AJAX requests, return a JSON response indicating logout
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'message' => __('Your session timed out due to inactivity. Please log in again.'),
'action' => 'logout',
'redirect_url' => LaravelLocalization::localizeUrl('/login')
], 419); // 419 Authentication Timeout
} else {
// For standard browser requests, redirect to login
$loginUrl = LaravelLocalization::localizeUrl('/login');
return redirect()->to($loginUrl)
->with('warning', __('Your session timed out due to inactivity. Please log in again.'));
}
}
// Get the specific timeout for the current profile type, or use the default of 120 min.
$configuredTimeout = timebank_config('profile_timeouts.' . $activeProfileType, timebank_config('profile_timeout_default', 120));
// Profile-specific timeout cannot exceed base session lifetime
// This ensures elevated profiles timeout at or before the base session expiration
$timeoutMinutes = min($configuredTimeout, $baseSessionLifetime);
// Log when timeout is being capped for debugging purposes
if ($configuredTimeout > $baseSessionLifetime) {
\Log::info('Profile timeout capped by SESSION_LIFETIME', [
'profile_type' => class_basename($activeProfileType),
'configured_timeout' => $configuredTimeout,
'session_lifetime' => $baseSessionLifetime,
'effective_timeout' => $timeoutMinutes,
]);
}
// Check if the timestamp exists and if the inactivity period has passed
if ($minutesSinceActivity >= $timeoutMinutes) {
$user = Auth::guard('web')->user();
// Check if the current active profile is already the User profile
if ($activeProfileType === \App\Models\User::class) {
// If the active profile is User and their session expires, log them out
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
// For AJAX requests, return a JSON response indicating logout
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'message' => __('Your session timed out due to inactivity. Please log in again.'),
'action' => 'logout', // Indicate a full logout
'redirect_url' => LaravelLocalization::localizeUrl('/login') // Provide login URL
], 419); // 419 Authentication Timeout
} else {
// For standard browser requests, redirect to login
$loginUrl = LaravelLocalization::localizeUrl('/login');
return redirect()->to($loginUrl)
->with('warning', __('Your session timed out due to inactivity. Please log in again.'));
}
} else {
// If the active profile is an elevated profile (Bank, Admin, etc.), switch back to User profile
Session::put([
'activeProfileType' => \App\Models\User::class,
'activeProfileId' => $user->id,
'activeProfileName' => $user->name,
'activeProfilePhoto' => $user->profile_photo_path,
'profile-switched-notification' => true,
]);
session(['notification.alert' => 'Your previous profile session timed out due to inactivity.']); // Will become a translation key in notification component
Session::forget('last_activity'); // Clear the timestamp
event(new \App\Events\ProfileSwitchEvent($user));
// Check if the request expects JSON (common for AJAX) or is an AJAX request
if ($request->expectsJson() || $request->ajax()) {
session(['notification.alert' => 'Your previous profile session timed out due to inactivity.']); // Will become a translation key in notification component
// Get the user's locale from the session (set by localization middleware)
$userLocale = Session::get('locale', config('app.fallback_locale'));
// Use LaravelLocalization to get the full, localized URL
$redirectUrl = LaravelLocalization::getURLFromRouteNameTranslated($userLocale, 'routes.main');
Session::save();
// Return a JSON response indicating timeout, use 419 status code
return response()->json([
'message' => __('Your previous profile session timed out due to inactivity.'),
'action' => 'redirect', // Change action name to 'redirect'
'redirect_url' => $redirectUrl // <-- Add URL to payload
], 419); // 419 Authentication Timeout
} else {
// For standard browser requests, perform the redirect with flash message
$mainUrl = LaravelLocalization::localizeUrl('/main-page');
return redirect()->to($mainUrl)
->with('warning', __('Your previous profile session timed out due to inactivity.'));
}
}
}
// Update the activity timestamp on relevant requests
// Exclude background heartbeats but allow user-initiated Livewire interactions
$isLivewireUpdate = $request->is(['*/livewire/update', 'livewire/update']);
$isUserInteraction = $isLivewireUpdate && $request->has('components');
if (!$request->is(['api/messenger/heartbeat']) && (!$isLivewireUpdate || $isUserInteraction)) {
Session::put('last_activity', now());
}
return $next($request); // Ensure the request is passed to the next middleware
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Laravel\Jetstream\Http\Middleware\AuthenticateSession;
class ConditionalAuthenticateSession
{
protected $authenticateSession;
public function __construct(AuthenticateSession $authenticateSession)
{
$this->authenticateSession = $authenticateSession;
}
public function handle(Request $request, Closure $next)
{
// Skip AuthenticateSession middleware in Docker environment
// as it causes session validation issues with session migration
if (env('IS_DOCKER', false)) {
return $next($request);
}
return $this->authenticateSession->handle($request, $next);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DisableAssetCacheInDebug
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if (config('app.debug')) {
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate');
$response->headers->set('Expires', '0');
}
return $response;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Symfony\Component\HttpFoundation\Response;
class EnsurePrinciplesAccepted
{
/**
* Routes that should be excluded from principles acceptance check.
*
* @var array
*/
protected $excludedRoutes = [
'static-principles',
'logout',
'verification.*',
'login',
'register',
'password.*',
'two-factor.*',
];
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Only check for authenticated users on the web guard
$user = Auth::guard('web')->user();
if (!$user) {
return $next($request);
}
// Check if the current route is excluded
foreach ($this->excludedRoutes as $excludedRoute) {
if ($request->routeIs($excludedRoute)) {
return $next($request);
}
}
// Check if user needs to accept or re-accept principles
if ($user->needsToReacceptPrinciples()) {
// Redirect to principles page with appropriate message
$principlesUrl = LaravelLocalization::localizeUrl(route('static-principles'));
$message = $user->hasAcceptedPrinciples()
? __('Our principles have been updated. Please review and accept the new version to continue.')
: __('Please accept the platform principles to continue.');
return redirect()->to($principlesUrl)
->with('warning', $message);
}
return $next($request);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class LogErrors
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($response->isClientError() || $response->isServerError()) {
$userId = Auth::check() ? Auth::id() : 'guest';
$ipAddress = $request->ip();
$datetime = now()->toDateTimeString();
$statusCode = $response->status();
$url = $request->fullUrl();
// Assign error message based on status code
$errorMessage = match ($statusCode) {
401 => '401 Error: Unauthorized',
403 => '403 Error: Forbidden',
404 => '404 Error: Page not found',
419 => '419 Error: Page expired',
429 => '429 Error: Too many requests',
500 => '500 Error: Server error',
503 => '503 Error: Service unavailable',
default => $statusCode . ' Error',
};
$context = [
'datetime' => $datetime,
'url' => $url,
'user_id' => $userId,
'activeProfileType' => session('activeProfileType'),
'activeProfileId' => session('activeProfileId'),
'ip_address' => $ipAddress,
];
// Add extra debugging for broadcasting auth errors
if (str_contains($url, 'broadcasting/auth') && $statusCode === 403) {
$context['channel_name'] = $request->input('channel_name');
$context['active_guard'] = session('active_guard');
$context['auth_guards'] = [
'web' => Auth::guard('web')->check(),
'admin' => Auth::guard('admin')->check(),
'bank' => Auth::guard('bank')->check(),
'organization' => Auth::guard('organization')->check(),
];
}
Log::warning($errorMessage, $context);
}
return $response;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
/**
* Profile-Based Session Timeout Middleware
*
* Enforces session timeouts based on the active profile type from platform configuration.
* This overrides the default SESSION_LIFETIME from .env with profile-specific timeouts.
*
* Configuration: config/timebank_cc.php -> 'profile_timeouts'
*/
class ProfileSessionTimeout
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Skip timeout check for guests
if (!Auth::check()) {
return $next($request);
}
// Get active profile from session
$activeProfileType = session('activeProfileType');
$activeProfileId = session('activeProfileId');
// Get profile-specific timeout from config
$timeoutMinutes = $this->getProfileTimeout($activeProfileType);
// Get last activity timestamp
$lastActivity = session('last_activity_at');
// If this is the first request, set last activity and continue
if (!$lastActivity) {
session(['last_activity_at' => now()->timestamp]);
return $next($request);
}
// Calculate idle time in minutes
$idleMinutes = (now()->timestamp - $lastActivity) / 60;
// Check if session has timed out
if ($idleMinutes > $timeoutMinutes) {
// Log the timeout for debugging
\Log::info('Session timeout', [
'user_id' => Auth::id(),
'profile_type' => $activeProfileType,
'profile_id' => $activeProfileId,
'idle_minutes' => round($idleMinutes, 2),
'timeout_limit' => $timeoutMinutes,
]);
// Clear session and logout
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// Redirect to login with timeout message
return redirect()->route('login')
->with('status', __('Your session has expired due to inactivity. Please log in again.'));
}
// Update last activity timestamp
session(['last_activity_at' => now()->timestamp]);
return $next($request);
}
/**
* Get the timeout duration in minutes for the given profile type
*
* @param string|null $profileType
* @return int Timeout in minutes
*/
protected function getProfileTimeout(?string $profileType): int
{
// Get profile_timeouts from platform config
$profileTimeouts = timebank_config('profile_timeouts', []);
// If profile type is set and has a specific timeout, use it
if ($profileType && isset($profileTimeouts[$profileType])) {
return (int) $profileTimeouts[$profileType];
}
// Otherwise, use the default timeout from platform config
$defaultTimeout = timebank_config('profile_timeout_default', 120);
// If still not set, fall back to SESSION_LIFETIME from .env
if (!$defaultTimeout) {
$defaultTimeout = Config::get('session.lifetime', 120);
}
return (int) $defaultTimeout;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
// Get the authenticated user from the current guard
$user = Auth::guard($guard)->user();
if ($user) {
session('activeProfileType', get_class($user));
session('activeProfileId', $user->id);
}
return redirect()->route('main'); // Redirect to the main route
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class RegistrationComplete
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
return $next($request);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Http\Middleware;
use App\Helpers\ProfileAuthorizationHelper;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
/**
* Require Admin Profile Middleware
*
* Ensures that only administrators or central banks can access admin routes.
* Prevents IDOR attacks and cross-guard access by validating profile ownership
* using ProfileAuthorizationHelper.
*
* Usage:
* - Apply to routes that require admin or central bank access
* - Validates active profile from session
* - Checks cross-guard attacks via ProfileAuthorizationHelper
* - Logs all access attempts for security monitoring
*/
class RequireAdminProfile
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Get active profile from session
$activeProfileType = session('activeProfileType');
$activeProfileId = session('activeProfileId');
if (!$activeProfileType || !$activeProfileId) {
Log::warning('RequireAdminProfile: No active profile in session', [
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
abort(403, __('No active profile selected'));
}
$profile = $activeProfileType::find($activeProfileId);
if (!$profile) {
Log::warning('RequireAdminProfile: Active profile not found', [
'active_profile_type' => $activeProfileType,
'active_profile_id' => $activeProfileId,
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
abort(403, __('Active profile not found'));
}
// Validate profile ownership using ProfileAuthorizationHelper
// This prevents cross-guard attacks (e.g., web user accessing admin profile)
ProfileAuthorizationHelper::authorize($profile);
// Verify admin or central bank permissions
if ($profile instanceof \App\Models\Admin) {
// Admin access OK
Log::info('RequireAdminProfile: Admin access granted', [
'admin_id' => $profile->id,
'admin_name' => $profile->name,
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
return $next($request);
}
if ($profile instanceof \App\Models\Bank) {
// Only central bank (level 0) can access admin routes
if ($profile->level === 0) {
Log::info('RequireAdminProfile: Central bank access granted', [
'bank_id' => $profile->id,
'bank_name' => $profile->name,
'bank_level' => $profile->level,
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
return $next($request);
}
Log::warning('RequireAdminProfile: Non-central bank attempted admin access', [
'bank_id' => $profile->id,
'bank_name' => $profile->name,
'bank_level' => $profile->level,
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
abort(403, __('Central bank access required for admin functions'));
}
// Not admin or central bank
Log::warning('RequireAdminProfile: Unauthorized profile type attempted admin access', [
'profile_type' => get_class($profile),
'profile_id' => $profile->id,
'ip_address' => $request->ip(),
'url' => $request->fullUrl(),
]);
abort(403, __('Admin or central bank access required'));
}
}

View File

@@ -0,0 +1,19 @@
<?php
// app/Http/Middleware/SetActiveGuard.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class SetActiveGuard
{
public function handle($request, Closure $next)
{
if (session()->has('active_guard')) {
Auth::shouldUse(session('active_guard'));
}
return $next($request);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class StoreUserLangPreference
{
/**
* Handle an incoming request.
* Store the active profile's language preference according to the current locale in the database.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$activeProfile = getActiveProfile();
if ($activeProfile) {
$currentLocale = LaravelLocalization::getCurrentLocale();
$profile_lang = $activeProfile->lang_preference;
// Check if this is an email verification route (both /email/verify/... and /*/email/verified)
$isVerificationRoute = $request->is('email/verify/*') || $request->is('*/email/verified');
// If this is the start of email verification flow, store original locale before updating
if ($isVerificationRoute && !session()->has('verification_original_locale')) {
session(['verification_original_locale' => $profile_lang]);
\Log::info('StoreUserLangPreference: Stored original locale for verification route', [
'route' => $request->path(),
'original_locale' => $profile_lang,
'current_locale' => $currentLocale,
]);
}
// Skip updating lang_preference during email verification flow
// to preserve the user's original locale
$isEmailVerificationFlow = session()->has('verification_original_locale');
if ($currentLocale && $currentLocale != $profile_lang && !$isEmailVerificationFlow) {
\Log::info('StoreUserLangPreference: Updating lang_preference', [
'profile_id' => $activeProfile->id,
'from' => $profile_lang,
'to' => $currentLocale,
]);
$activeProfile->update(['lang_preference' => $currentLocale]);
} elseif ($isEmailVerificationFlow) {
\Log::info('StoreUserLangPreference: Skipping update during verification flow', [
'profile_id' => $activeProfile->id,
'current_locale' => $currentLocale,
'profile_lang' => $profile_lang,
]);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,40 @@
<?php
// 2. Middleware for automatic presence tracking
namespace App\Http\Middleware;
use App\Services\PresenceService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class TrackUserPresence
{
protected $presenceService;
public function __construct(PresenceService $presenceService)
{
$this->presenceService = $presenceService;
}
public function handle(Request $request, Closure $next, $guard = null)
{
// If no guard specified, use the active guard from session (defaults to 'web')
// This ensures we only track presence for the currently active profile
$activeGuard = $guard ?? $request->session()->get('active_guard', 'web');
if (auth($activeGuard)->check()) {
$user = auth($activeGuard)->user();
$cacheKey = "presence_last_update_{$activeGuard}_{$user->id}";
$lastUpdate = Cache::get($cacheKey);
if (!$lastUpdate || now()->diffInSeconds($lastUpdate) > timebank_config('presence_settings.update_interval', 60)) { // Only update using this interval
$this->presenceService->updatePresence($user, $activeGuard);
Cache::put($cacheKey, now(), 300);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class UpdateSessionGuard
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
/**
* Handle tasks after the response has been sent to the browser.
*
* This method updates both the guard and user_id columns in the sessions table
* to reflect which authentication guard is currently active for the session.
* This prevents conflicts when multiple profile types (User, Bank, Organization)
* share the same ID number.
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
* @return void
*/
public function terminate(Request $request, Response $response): void
{
// Only update if using database sessions
if (config('session.driver') !== 'database') {
return;
}
// Get the session ID
$sessionId = $request->session()->getId();
if (!$sessionId) {
return;
}
// Get the active guard from session (defaults to 'web')
$activeGuard = $request->session()->get('active_guard', 'web');
// Get the authenticated user ID for the active guard
$userId = \Illuminate\Support\Facades\Auth::guard($activeGuard)->id();
// If no user is authenticated on the active guard, fall back to default guard
if (!$userId) {
$userId = \Illuminate\Support\Facades\Auth::id();
}
// Update both guard and user_id columns in the sessions table
try {
$updateData = ['guard' => $activeGuard];
// Only update user_id if we have one
if ($userId) {
$updateData['user_id'] = $userId;
}
DB::table(config('session.table', 'sessions'))
->where('id', $sessionId)
->update($updateData);
} catch (\Exception $e) {
// Silently fail - don't break the application if guard column doesn't exist yet
// This can happen during migration or if the migration hasn't run
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}