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