Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9195f62
#863 Fix controller dash navbar formatting
iccowan Feb 10, 2026
75dcc41
#863 Lay framework for impersonation
iccowan Feb 10, 2026
6e39399
#863 Add routes for impersonation and set session var
iccowan Feb 10, 2026
bd20657
#863 Implement impersonation
iccowan Feb 10, 2026
2ba8002
#863 Add warning to users when impersonating
iccowan Feb 10, 2026
6e5571a
#863 Filter out inactive users from impersonation
iccowan Feb 10, 2026
7ce78ec
#863 Add visitor flag to impersonation list
iccowan Feb 10, 2026
66d1c7b
#863 Put impersonation behind feature toggle
iccowan Feb 10, 2026
44aa20b
#863 Fix formatting
iccowan Feb 15, 2026
d9bc82a
#863 Add impersonation to audit log
iccowan Feb 15, 2026
1a51287
#863 Disable changing impersonation when impersonating
iccowan Feb 15, 2026
99cee5c
#863 Track impersonation start/end in audit log
iccowan Feb 15, 2026
ec3d260
Fix formatting
iccowan Feb 23, 2026
6796661
#863 Impersonation for web staff only
iccowan Feb 23, 2026
af90bf2
#863 Remove started impersonation message
iccowan Feb 28, 2026
6bd58f5
#863 Add permanent impersonation warning banner
iccowan Feb 28, 2026
1e1bfaf
#863 Remove old end impersonation button
iccowan Feb 28, 2026
90a8187
Merge branch 'main' into 863-impersonation-implementation
kjporter Mar 12, 2026
75307f4
Fix typo
kjporter Mar 12, 2026
e5603cd
Update Impersonation.php
kjporter Mar 13, 2026
9d15450
Update impersonation_warning.blade.php
kjporter Mar 15, 2026
d6823d5
Update Impersonation.php
kjporter Mar 15, 2026
681ef2c
Revert "Update Impersonation.php"
kjporter Mar 15, 2026
5ca2c11
#863 Fix middleware logic for browser test
iccowan Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/Audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App;

use App\Enums\SessionVariables;
use Auth;
use Illuminate\Database\Eloquent\Model;

Expand All @@ -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();
}
}
1 change: 1 addition & 0 deletions app/Enums/FeatureToggles.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
3 changes: 3 additions & 0 deletions app/Enums/SessionVariables.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
34 changes: 34 additions & 0 deletions app/Http/Controllers/ImpersonationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Http\Controllers;

use App\Audit;
use App\Enums\SessionVariables;
use App\User;
use Illuminate\Http\Request;

class ImpersonationController extends Controller {
public function start(Request $request) {
$user = User::find($request->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');
}
}
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
Expand Down
26 changes: 26 additions & 0 deletions app/Http/Middleware/Impersonation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Http\Middleware;

use App\Enums\FeatureToggles;
use App\Enums\SessionVariables;
use Auth;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class Impersonation {
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response {
if (toggleEnabled(FeatureToggles::IMPERSONATION) && session()->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);
}
}
2 changes: 2 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
34 changes: 34 additions & 0 deletions app/View/Composers/ImpersonationComposer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\View\Composers;

use App\Enums\FeatureToggles;
use App\Enums\SessionVariables;
use App\User;
use Auth;
use Illuminate\View\View;

class ImpersonationComposer {
/**
* Create a new profile composer.
*/
public function __construct(
) {
}

/**
* Bind data to the view.
*/
public function compose(View $view): void {
if (toggleEnabled(FeatureToggles::IMPERSONATION)) {
$users = null;
$is_impersonating = session()->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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void {
Schema::table('audits', function (Blueprint $table) {
$table->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');
});
}
};
43 changes: 22 additions & 21 deletions resources/views/inc/dashboard_head.blade.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
@include('inc.logo', ['color' => 'black'])
</a>
<div class="d-flex justify-content-start ms-5 collapse navbar-collapse">

{{ html()->form()->route('searchAirport')->class(['row','row-cols-lg-auto'])->open() }}
<div class="col-12 input-group">
{{ html()->text('apt', null)->placeholder('Search Airport ICAO')->class(['form-control']) }}
&nbsp;
<button class="btn btn-success" type="submit">Search</button>
</div>
<ul class="navbar-nav me-auto align-items-center">
{{ html()->form()->route('searchAirport')->class(['form-inline'])->open() }}
<div class="col-12 input-group">
{{ html()->text('apt', null)->placeholder('Search Airport ICAO')->class(['form-control']) }}
&nbsp;
<button class="btn btn-success" type="submit">Search</button>
</div>
{{ html()->form()->close() }}
</ul>
<ul class="navbar-nav ml-auto align-items-center">
<a class="nav-link {{ Nav::isRoute('controller_dash_home') }}" href="/dashboard">Dashboard Home</a>
@if(toggleEnabled($FeatureToggles::IMPERSONATION) && (Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm')))
{{ html()->form()->route('startImpersonation')->class(['form-inline'])->open() }}
{{ html()->select('user_id', $users, Auth::id())->class(['form-select'])->attributes(['onchange' => 'this.form.submit()'])->disabled($is_impersonating) }}
{{ html()->form()->close() }}
</div>

<ul class="navbar-nav ms-auto">
<a class="nav-link {{ Nav::isRoute('controller_dash_home') }}" href="/dashboard">Dashboard Home</a>
<li class="nav-item dropdown">
<a class="nav-link" style="pointer-events:none">{{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }}</a>
</li>
</ul>
</div>
</div>
</nav>
@else
<a class="nav-link disabled">{{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }}</a>
@endif
</ul>
</div>
</nav>
6 changes: 6 additions & 0 deletions resources/views/inc/impersonation_warning.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@if(isset($is_impersonating) && $is_impersonating && toggleEnabled($FeatureToggles::IMPERSONATION))
<br>
<a class="d-block alert alert-warning" href="/dashboard/admin/impersonation/stop">
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.
</a>
@endif
7 changes: 7 additions & 0 deletions resources/views/inc/messages.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
</div>
@endif

@if(session()->has($SessionVariables::WARNING->value))
<br>
<div class="alert alert-warning">
{{ session($SessionVariables::WARNING->value) }}
</div>
@endif

@if(session()->has($SessionVariables::ERROR->value))
<br>
<div class="alert alert-danger">
Expand Down
5 changes: 5 additions & 0 deletions resources/views/layouts/dashboard.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
</title>
</head>
<body>
{{-- Impersonation Warning --}}
<div class="container">
@include('inc.impersonation_warning')
</div>

{{-- Messages --}}
<div class="container">
@include('inc.messages')
Expand Down
4 changes: 4 additions & 0 deletions resources/views/layouts/master.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
</title>
</head>
<body>
{{-- Impersonation Warning --}}
<div class="container">
@include('inc.impersonation_warning')
</div>

{{-- Messages --}}
<div class="container-fluid bg-secondary">
Expand Down
5 changes: 5 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
/*
Expand Down
Loading