diff --git a/app/Enums/UserFlags.php b/app/Enums/UserFlags.php index f85902b..3d23ff5 100644 --- a/app/Enums/UserFlags.php +++ b/app/Enums/UserFlags.php @@ -6,4 +6,5 @@ enum UserFlags: string { case HEAD_DIRECTOR = 'head_director'; case MONITOR = 'monitor'; + case CAN_IMPERSONATE = 'can_impersonate'; } diff --git a/app/Http/Controllers/Admin/ImpersonationController.php b/app/Http/Controllers/Admin/ImpersonationController.php new file mode 100644 index 0000000..756d982 --- /dev/null +++ b/app/Http/Controllers/Admin/ImpersonationController.php @@ -0,0 +1,78 @@ +user_id); + $admin = $request->user(); + + if (! $admin->can('impersonate', $user)) { + abort(403); + } + + // Prevent impersonating yourself or impersonating if already impersonating + if ($admin->id === $user->id || session()->has('impersonator_id')) { + return back()->with('error', 'Cannot impersonate.'); + } + + // Save the original admin id and optionally guard + session()->put('impersonator_id', $admin->id); + session()->put('impersonator_started_at', now()->toDateTimeString()); + + auditionLog('Started impersonating '.$user->full_name().' - '.$user->email, ['users' => [$user->id]]); + + // Switch user + Auth::loginUsingId($user->getAuthIdentifier()); + + // Regenerate session to mitigate fixation + $request->session()->regenerate(); + + return redirect(route('dashboard'))->with('success', 'Now impersonating '.$user->email); + } + + public function stop(Request $request) + { + $impersonatedUser = Auth::user(); + $impersonatorId = session('impersonator_id'); + if (! $impersonatorId) { + return back()->with('error', 'Not impersonating.'); + } + + // Restore original admin + $admin = User::find($impersonatorId); + if ($admin) { + Auth::loginUsingId($admin->getAuthIdentifier()); + } else { + // If admin was deleted, just log out + Auth::logout(); + } + + auditionLog('Stopped impersonating '.$impersonatedUser->full_name().' - '.$impersonatedUser->email, ['users' => [$impersonatedUser->id]]); + + // Clear impersonation data + session()->forget(['impersonator_id', 'impersonator_started_at']); + + // Regenerate session + $request->session()->regenerate(); + + return redirect(route('dashboard'))->with('success', 'Stopped impersonation.'); + } + + public function index() + { + + $users = User::where('id', '!=', auth()->id())->get(); + + return view('admin.impersonation.index', compact('users')); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c7288e0..7b408f5 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -38,6 +38,7 @@ use App\Services\EntryService; use App\Services\ScoreService; use App\Services\UserService; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -78,6 +79,9 @@ class AppServiceProvider extends ServiceProvider Student::observe(StudentObserver::class); User::observe(UserObserver::class); + Gate::define('impersonate', function ($admin, $target) { + return $admin->hasFlag('can_impersonate') && $admin->id !== $target->id; + }); // Model::preventLazyLoading(! app()->isProduction()); } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 2d54ccf..b929be9 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -65,5 +65,6 @@ class FortifyServiceProvider extends ServiceProvider Fortify::verifyEmailView(function () { return view('auth.verify-email'); }); + } } diff --git a/resources/views/admin/impersonation/index.blade.php b/resources/views/admin/impersonation/index.blade.php new file mode 100644 index 0000000..bed32eb --- /dev/null +++ b/resources/views/admin/impersonation/index.blade.php @@ -0,0 +1,15 @@ + + Impersonation + + Select User to Impersonate + + + User to impersonate + @foreach ($users as $user) + + @endforeach + + + + + diff --git a/resources/views/components/layout/app.blade.php b/resources/views/components/layout/app.blade.php index 8d9415b..2b98d95 100644 --- a/resources/views/components/layout/app.blade.php +++ b/resources/views/components/layout/app.blade.php @@ -19,6 +19,14 @@ merge(['class' => 'h-full']) }}>
+ @if(session()->has('impersonator_id')) +
+ Currently impersonating {{ auth()->user()->full_name() }} + + + +
+ @endif {{-- @if(request()->is('*admin*'))--}} {{-- --}} {{-- @else--}} diff --git a/routes/admin.php b/routes/admin.php index 62aeec4..9a32fd6 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Admin\EntryController; use App\Http\Controllers\Admin\EventController; use App\Http\Controllers\Admin\ExportEntriesController; use App\Http\Controllers\Admin\ExportResultsController; +use App\Http\Controllers\Admin\ImpersonationController; use App\Http\Controllers\Admin\PrelimDefinitionController; use App\Http\Controllers\Admin\PrintCards; use App\Http\Controllers\Admin\PrintRoomAssignmentsController; @@ -230,4 +231,17 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::patch('/{splitScoreDefinition}', 'update')->name('admin.split_score_definitions.update'); Route::delete('/{splitScoreDefinition}', 'destroy')->name('admin.split_score_definitions.destroy'); }); + }); + +// Impersonation Routes +Route::middleware(['auth', 'verified', CheckIfAdmin::class])->get('su/', + [ImpersonationController::class, 'index']) + ->name('impersonate.index'); + +Route::middleware(['auth', 'verified', CheckIfAdmin::class])->post('su/start', + [ImpersonationController::class, 'start']) + ->name('impersonate.start'); + +Route::post('su/stop', [ImpersonationController::class, 'stop']) + ->name('impersonate.stop');