Initial commit
This commit is contained in:
37
app/Http/Middleware/AuthAnyGuard.php
Normal file
37
app/Http/Middleware/AuthAnyGuard.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/Authenticate.php
Normal file
22
app/Http/Middleware/Authenticate.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
80
app/Http/Middleware/AuthenticateAdmin.php
Normal file
80
app/Http/Middleware/AuthenticateAdmin.php
Normal 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);
|
||||
}
|
||||
}
|
||||
51
app/Http/Middleware/AuthenticateBank.php
Normal file
51
app/Http/Middleware/AuthenticateBank.php
Normal 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);
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/CanOnWebGuard.php
Normal file
22
app/Http/Middleware/CanOnWebGuard.php
Normal 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);
|
||||
}
|
||||
}
|
||||
175
app/Http/Middleware/CheckProfileInactivity.php
Normal file
175
app/Http/Middleware/CheckProfileInactivity.php
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
28
app/Http/Middleware/ConditionalAuthenticateSession.php
Normal file
28
app/Http/Middleware/ConditionalAuthenticateSession.php
Normal 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);
|
||||
}
|
||||
}
|
||||
21
app/Http/Middleware/DisableAssetCacheInDebug.php
Normal file
21
app/Http/Middleware/DisableAssetCacheInDebug.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal 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 = [
|
||||
//
|
||||
];
|
||||
}
|
||||
64
app/Http/Middleware/EnsurePrinciplesAccepted.php
Normal file
64
app/Http/Middleware/EnsurePrinciplesAccepted.php
Normal 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);
|
||||
}
|
||||
}
|
||||
70
app/Http/Middleware/LogErrors.php
Normal file
70
app/Http/Middleware/LogErrors.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal 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 = [
|
||||
//
|
||||
];
|
||||
}
|
||||
106
app/Http/Middleware/ProfileSessionTimeout.php
Normal file
106
app/Http/Middleware/ProfileSessionTimeout.php
Normal 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;
|
||||
}
|
||||
}
|
||||
39
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
39
app/Http/Middleware/RedirectIfAuthenticated.php
Normal 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);
|
||||
}
|
||||
}
|
||||
21
app/Http/Middleware/RegistrationComplete.php
Normal file
21
app/Http/Middleware/RegistrationComplete.php
Normal 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);
|
||||
}
|
||||
}
|
||||
109
app/Http/Middleware/RequireAdminProfile.php
Normal file
109
app/Http/Middleware/RequireAdminProfile.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/SetActiveGuard.php
Normal file
19
app/Http/Middleware/SetActiveGuard.php
Normal 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);
|
||||
}
|
||||
}
|
||||
62
app/Http/Middleware/StoreUserLangPreference.php
Normal file
62
app/Http/Middleware/StoreUserLangPreference.php
Normal 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);
|
||||
}
|
||||
}
|
||||
40
app/Http/Middleware/TrackUserPresence.php
Normal file
40
app/Http/Middleware/TrackUserPresence.php
Normal 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);
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/TrimStrings.php
Normal file
19
app/Http/Middleware/TrimStrings.php
Normal 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',
|
||||
];
|
||||
}
|
||||
20
app/Http/Middleware/TrustHosts.php
Normal file
20
app/Http/Middleware/TrustHosts.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Middleware/TrustProxies.php
Normal file
28
app/Http/Middleware/TrustProxies.php
Normal 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;
|
||||
}
|
||||
76
app/Http/Middleware/UpdateSessionGuard.php
Normal file
76
app/Http/Middleware/UpdateSessionGuard.php
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
app/Http/Middleware/VerifyCsrfToken.php
Normal 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 = [
|
||||
//
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user