diff --git a/app/Audit.php b/app/Audit.php index 86769df98..23758fafb 100644 --- a/app/Audit.php +++ b/app/Audit.php @@ -2,6 +2,7 @@ namespace App; +use App\Enums\SessionVariables; use Auth; use Illuminate\Database\Eloquent\Model; @@ -17,10 +18,21 @@ public function getTimeDateAttribute() { } public static function newAudit(string $message): void { + $impersonated_by_id = null; + $impersonation_string = ''; + if (session()->has(SessionVariables::IMPERSONATING_USER->value)) { + $impersonated_by_id = session(SessionVariables::IMPERSONATING_USER->value); + $impersonation_user = User::find($impersonated_by_id); + + $impersonation_string = 'IMPERSONATED BY ' . (is_null($impersonation_user) ? 'UNKNOWN' : $impersonation_user->full_name) . ': '; + } + $impersonated_by_id = session()->has(SessionVariables::IMPERSONATING_USER->value) ? session(SessionVariables::IMPERSONATING_USER->value) : null; + $audit = new Audit; $audit->cid = Auth::id(); + $audit->impersonated_by_id = $impersonated_by_id; $audit->ip = $_SERVER['REMOTE_ADDR']; - $audit->what = Auth::user()->full_name . ' ' . $message; + $audit->what = $impersonation_string . Auth::user()->full_name . ' ' . $message; $audit->save(); } } diff --git a/app/Enums/FeatureToggles.php b/app/Enums/FeatureToggles.php index 75e563349..b5b7852ff 100644 --- a/app/Enums/FeatureToggles.php +++ b/app/Enums/FeatureToggles.php @@ -10,4 +10,5 @@ enum FeatureToggles: string { case CUSTOM_THEME_LOGO = 'custom_theme_logo'; case LOCAL_HERO = 'local-hero'; case AUTO_SUPPORT_EVENTS = 'auto_support_events'; + case IMPERSONATION = 'impersonation'; } diff --git a/app/Enums/SessionVariables.php b/app/Enums/SessionVariables.php index b50691b0e..028406d4f 100644 --- a/app/Enums/SessionVariables.php +++ b/app/Enums/SessionVariables.php @@ -4,8 +4,11 @@ enum SessionVariables: string { case SUCCESS = 'success'; + case WARNING = 'warning'; case ERROR = 'error'; case VATSIM_AUTH_STATE = 'vatsimauthstate'; case REALOPS_PILOT_REDIRECT = 'pilot_redirect'; case REALOPS_PILOT_REDIRECT_PATH = 'pilot_redirect_path'; + case IMPERSONATE = 'impersonate'; + case IMPERSONATING_USER = 'impersonating_user'; } diff --git a/app/Http/Controllers/ImpersonationController.php b/app/Http/Controllers/ImpersonationController.php new file mode 100644 index 000000000..9c3c5ef0a --- /dev/null +++ b/app/Http/Controllers/ImpersonationController.php @@ -0,0 +1,34 @@ +user_id); + $is_impersonating = session()->has(SessionVariables::IMPERSONATE->value); + if (is_null($user)) { + return redirect()->back()->with(SessionVariables::ERROR->value, 'That user does not exist'); + } + + if ($is_impersonating) { + return redirect()->back()->with(SessionVariables::ERROR->value, 'You must first stop impersonating your current user before beginning a new session'); + } + + session()->put(SessionVariables::IMPERSONATE->value, $user->id); + Audit::newAudit('started impersonating user ' . $user->impersonation_name . '.'); + + return redirect('/dashboard'); + } + + public function stop() { + Audit::newAudit('impersonation session ending...'); + + session()->forget(SessionVariables::IMPERSONATE->value); + return redirect('/dashboard'); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 869e64d7e..a86084528 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -35,6 +35,7 @@ class Kernel extends HttpKernel { \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\Impersonation::class, ], 'api' => [ diff --git a/app/Http/Middleware/Impersonation.php b/app/Http/Middleware/Impersonation.php new file mode 100644 index 000000000..93f502dd1 --- /dev/null +++ b/app/Http/Middleware/Impersonation.php @@ -0,0 +1,26 @@ +has(SessionVariables::IMPERSONATE->value) && Auth::check() && (Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm'))) { + session()->put(SessionVariables::IMPERSONATING_USER->value, Auth::id()); + Auth::onceUsingId(session(SessionVariables::IMPERSONATE->value)); + } + + return $next($request); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c7df652b2..347941f6d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Enums\FeatureToggles; use App\Enums\SessionVariables; +use App\View\Composers\ImpersonationComposer; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; @@ -29,6 +30,7 @@ public function boot(): void { View::share('FeatureToggles', FeatureToggles::class); View::share('SessionVariables', SessionVariables::class); + View::composer(['inc.dashboard_head', 'inc.impersonation_warning'], ImpersonationComposer::class); /** * Paginate a standard Laravel Collection. diff --git a/app/User.php b/app/User.php index 08bb784ff..837ac01e4 100644 --- a/app/User.php +++ b/app/User.php @@ -73,6 +73,22 @@ public function getFullNameRatingAttribute() { return $this->full_name . ' - ' . $this->rating_short; } + public function getImpersonationNameAttribute() { + $roles = array_reduce($this->roles->toArray(), function ($role_string, $role) { + return $role_string . $role['name'] . ', '; + }, ''); + + if ($this->visitor) { + $roles = 'visitor'; + } + + if ($roles != '') { + $roles = ' (' . trim($roles, ', ') . ')'; + } + + return $this->backwards_name . ' ' . $this->id . ' - ' . $this->rating_short . $roles; + } + public static $RatingShort = [ 0 => 'N/A', 1 => 'OBS', 2 => 'S1', diff --git a/app/View/Composers/ImpersonationComposer.php b/app/View/Composers/ImpersonationComposer.php new file mode 100644 index 000000000..78d757db1 --- /dev/null +++ b/app/View/Composers/ImpersonationComposer.php @@ -0,0 +1,34 @@ +has(SessionVariables::IMPERSONATE->value); + + if (Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm')) { + $users = User::where('status', 1)->orderBy('lname', 'ASC')->get()->pluck('impersonation_name', 'id'); + } + + $view->with('users', $users)->with('is_impersonating', $is_impersonating); + } + } +} diff --git a/database/migrations/2026_02_23_141539_update_audits_add_impersonated_by_id_column.php b/database/migrations/2026_02_23_141539_update_audits_add_impersonated_by_id_column.php new file mode 100644 index 000000000..8e409dd05 --- /dev/null +++ b/database/migrations/2026_02_23_141539_update_audits_add_impersonated_by_id_column.php @@ -0,0 +1,27 @@ +integer('impersonated_by_id')->nullable(); + + $table->foreign('impersonated_by_id')->references('id')->on('roster')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::table('audits', function (Blueprint $table) { + $table->dropColumn('impersonated_by_id'); + }); + } +}; diff --git a/resources/views/inc/dashboard_head.blade.php b/resources/views/inc/dashboard_head.blade.php index 6fde59c09..d872ae0ea 100644 --- a/resources/views/inc/dashboard_head.blade.php +++ b/resources/views/inc/dashboard_head.blade.php @@ -1,25 +1,26 @@ - + @else + {{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }} + @endif + + + diff --git a/resources/views/inc/impersonation_warning.blade.php b/resources/views/inc/impersonation_warning.blade.php new file mode 100644 index 000000000..fb26418f0 --- /dev/null +++ b/resources/views/inc/impersonation_warning.blade.php @@ -0,0 +1,6 @@ +@if(isset($is_impersonating) && $is_impersonating && toggleEnabled($FeatureToggles::IMPERSONATION)) +
+ + WARNING: You are currently impersonating a user. Use extreme caution as any action you perform will be performed as that user, and will be tracked as so with you as the impersonator. This should be used for debugging and development only! Click this warning at any time to end impersonation. + +@endif diff --git a/resources/views/inc/messages.blade.php b/resources/views/inc/messages.blade.php index 5b696ce7d..99321fa1b 100644 --- a/resources/views/inc/messages.blade.php +++ b/resources/views/inc/messages.blade.php @@ -16,6 +16,13 @@ @endif +@if(session()->has($SessionVariables::WARNING->value)) +
+
+ {{ session($SessionVariables::WARNING->value) }} +
+@endif + @if(session()->has($SessionVariables::ERROR->value))
diff --git a/resources/views/layouts/dashboard.blade.php b/resources/views/layouts/dashboard.blade.php index 1e0422232..11b022130 100644 --- a/resources/views/layouts/dashboard.blade.php +++ b/resources/views/layouts/dashboard.blade.php @@ -31,6 +31,11 @@ + {{-- Impersonation Warning --}} +
+ @include('inc.impersonation_warning') +
+ {{-- Messages --}}
@include('inc.messages') diff --git a/resources/views/layouts/master.blade.php b/resources/views/layouts/master.blade.php index e9efc5079..dde46177c 100644 --- a/resources/views/layouts/master.blade.php +++ b/resources/views/layouts/master.blade.php @@ -34,6 +34,10 @@ + {{-- Impersonation Warning --}} +
+ @include('inc.impersonation_warning') +
{{-- Messages --}}
diff --git a/routes/web.php b/routes/web.php index 43187ccaa..39f42f6cb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -333,6 +333,11 @@ Route::prefix('monitor')->middleware('permission:staff')->group(function () { Route::get('/', 'AdminDash@backgroundMonitor'); }); + + Route::prefix('impersonation')->middleware('toggle:impersonation')->group(function () { + Route::post('/', 'ImpersonationController@start')->middleware('role:wm|awm')->name('startImpersonation'); + Route::get('/stop', 'ImpersonationController@stop')->name('stopImpersonation'); + }); }); }); /*