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))
+
+