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..ea0a800f6 --- /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')->with(SessionVariables::ERROR->value, 'Successfully started impersonationg ' . $user->full_name . '. CAUTION: Impersonating actively logs you into the user\'s REAL account. Changes made while impersonating will be reflected on the user\'s actual account. PROCEED WITH CARE.'); + } + + 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..96d40193b --- /dev/null +++ b/app/Http/Middleware/Impersonation.php @@ -0,0 +1,26 @@ +has(SessionVariables::IMPERSONATE->value) && 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..9e0904ec9 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', 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..4b1ba0d12 100644 --- a/resources/views/inc/dashboard_head.blade.php +++ b/resources/views/inc/dashboard_head.blade.php @@ -1,25 +1,29 @@ - + @else + {{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }} + @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/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'); + }); }); }); /*