Skip to content

Commit 3f62c84

Browse files
committed
Added option for admins to impersonate other users
Inspired by: https://github.com/nextcloud/impersonate
1 parent 9933713 commit 3f62c84

File tree

8 files changed

+231
-2
lines changed

8 files changed

+231
-2
lines changed

app/Http/Controllers/AdminController.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,4 +665,54 @@ public function showThemes(request $request)
665665
{
666666
return view('/panel/theme');
667667
}
668+
669+
//Removes impersonation if authenticated
670+
public function authAs(request $request)
671+
{
672+
673+
$userID = $request->id;
674+
$token = $request->token;
675+
676+
$user = User::find($userID);
677+
678+
if($user->remember_token == $token){
679+
$user->auth_as = null;
680+
$user->remember_token = null;
681+
$user->save();
682+
683+
setcookie("display_auth_nav", "", time() - 3600, "/");
684+
685+
Auth::loginUsingId($userID);
686+
687+
return redirect('/admin/users/all');
688+
} else {
689+
return redirect('');
690+
}
691+
692+
}
693+
694+
//Removes impersonation if authenticated
695+
public function authAsID(request $request)
696+
{
697+
698+
$adminUser = User::whereNotNull('auth_as')->where('role', 'admin')->first();
699+
700+
if (!$adminUser) {
701+
702+
$userID = $request->id;
703+
$id = Auth::user()->id;
704+
705+
$user = User::find($id);
706+
707+
$user->auth_as = $userID;
708+
$user->save();
709+
710+
return redirect('dashboard');
711+
712+
} else {
713+
return redirect('admin/users/all');
714+
}
715+
716+
}
717+
668718
}

app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,6 @@ class Kernel extends HttpKernel
6565
'admin' => \App\Http\Middleware\admin::class,
6666
'blocked' => \App\Http\Middleware\CheckBlockedUser::class,
6767
'max.users' => \App\Http\Middleware\MaxUsers::class,
68+
'impersonate' => \App\Http\Middleware\Impersonate::class,
6869
];
6970
}

app/Http/Middleware/Impersonate.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
use Illuminate\Support\Facades\Auth;
5+
use Illuminate\Support\Str;
6+
use App\Models\User;
7+
use Closure;
8+
9+
class Impersonate
10+
{
11+
public function handle($request, Closure $next)
12+
{
13+
$adminUser = User::whereNotNull('auth_as')->where('role', 'admin')->first();
14+
15+
if ($adminUser) {
16+
17+
$originalUser = $adminUser->id;
18+
19+
$id = is_numeric($adminUser->auth_as) ? $adminUser->auth_as : $adminUser->id;
20+
$user = User::find($id);
21+
22+
$name = $user->name;
23+
24+
if(Auth::user()->id === $originalUser) {
25+
26+
// Generate unique token
27+
$token = Str::random(60);
28+
if(\Route::currentRouteName() !== 'authAs'){
29+
$adminUser->remember_token = $token;
30+
$adminUser->save();
31+
echo "<script>window.location.href = '".url('studio/links')."';</script>";
32+
}
33+
34+
Auth::loginUsingId($id);
35+
setcookie("display_auth_nav", "true", time() + (10 * 365 * 24 * 60 * 60), "/");
36+
}
37+
38+
if(isset($_COOKIE['display_auth_nav'])) {
39+
if (file_exists(base_path(findAvatar($id)))) {
40+
$img = '<img alt="avatar" class="iimg irounded" src="' . url(findAvatar($id)) . '">';
41+
} elseif (file_exists(base_path("assets/linkstack/images/").findFile('avatar'))) {
42+
$img = '<img alt="avatar" class="iimg irounded" src="' . url("assets/linkstack/images/") . "/" . findFile('avatar') . '">';
43+
} else {
44+
$img = '<img alt="avatar" class="iimg" src="' . asset('assets/linkstack/images/logo.svg') . '">';
45+
}
46+
$dashboard = url('dashboard');
47+
$URL = url('/auth-as');
48+
$csrf = csrf_token();
49+
$remember_token = User::find($originalUser);
50+
$token = $remember_token->remember_token;
51+
$customHtml =
52+
<<<EOD
53+
54+
<style>
55+
.ibar {
56+
position: fixed;
57+
top: 0;
58+
left: 0;
59+
width: 100%;
60+
height: 67px;
61+
background-color: #4d4c51;
62+
z-index: 911;
63+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
64+
}
65+
66+
.itext1 {
67+
color: white;
68+
font-family: "Inter", sans-serif;
69+
font-size: 18px;
70+
display: flex;
71+
align-items: center;
72+
justify-content: space-between;
73+
padding: 17px 16px;
74+
}
75+
76+
.itext1 span a {
77+
display: flex;
78+
align-items: center;
79+
justify-content: space-between;
80+
}
81+
82+
.itext1 a {
83+
color: white;
84+
text-decoration: none;
85+
}
86+
87+
.itext1 svg {
88+
width: 32px;
89+
height: 32px;
90+
fill: currentColor;
91+
margin-left: 8px;
92+
margin-bottom: 4px;
93+
}
94+
95+
.iimg {
96+
width: 32px;
97+
height: 32px;
98+
margin-right: 8px;
99+
margin-bottom: 3px;
100+
}
101+
102+
.irounded {
103+
border-radius: 50%;
104+
}
105+
106+
body {
107+
padding-top: 60px; /* Add padding equal to the height of .ibar */
108+
}
109+
</style>
110+
111+
<div class="ibar">
112+
<p class="itext1">
113+
<span>
114+
<a href="$dashboard">$img $name</a>
115+
</span>
116+
<a style="cursor:pointer" onclick="document.getElementById('submitForm').submit(); return false;">
117+
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-x" viewBox="0 0 16 16">
118+
<path
119+
d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"
120+
/>
121+
</svg>
122+
</a>
123+
</p>
124+
</div>
125+
126+
<form id="submitForm" action="$URL" method="POST" style="display: none;">
127+
<input type="hidden" name="_token" value="$csrf">
128+
<input type="hidden" name="token" value="$token">
129+
<input type="hidden" name="id" value="$originalUser">
130+
</form>
131+
132+
<script>
133+
function submitForm() {
134+
document.getElementById('submitForm').submit();
135+
}
136+
</script>
137+
138+
EOD;;
139+
} else {$customHtml = "";}
140+
141+
$response = $next($request);
142+
$content = $response->getContent();
143+
$modifiedContent = preg_replace('/<body([^>]*)>/', "<body$1>{$customHtml}", $content);
144+
$response->setContent($modifiedContent);
145+
146+
return $response;
147+
} else {
148+
if(isset($_COOKIE['display_auth_nav'])) {
149+
setcookie("display_auth_nav", "", time() - 3600, "/");
150+
Auth::logout();
151+
}
152+
return $next($request);
153+
}
154+
}
155+
}

database/migrations/2014_10_12_000000_create_users_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function up()
2626
$table->rememberToken();
2727
$table->timestamps();
2828
$table->string('theme')->nullable();
29+
$table->unsignedBigInteger('auth_as')->nullable();
2930
});
3031
}
3132

resources/lang/en/messages.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@
521521

522522
# Tooltips
523523
'tt.Delete' => 'Delete',
524+
'tt.Impersonate' => 'Impersonate',
524525
'tt.Edit' => 'Edit',
525526
'tt.All links' => 'All links',
526527

resources/views/components/finishing.blade.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@
154154
} catch (exception $e) {}
155155
Schema::enableForeignKeyConstraints();
156156
157+
// Adds new column to the users table
158+
try {
159+
if (!Schema::hasColumn('users', 'auth_as')) {
160+
Schema::table('users', function (Blueprint $table) {
161+
$table->unsignedBigInteger('auth_as')->nullable();
162+
});
163+
}} catch (exception $e) {}
164+
157165
try {
158166
DB::table('link_types')->updateOrInsert([
159167
'typename' => 'text',

resources/views/panel/users.blade.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<?php use App\Models\User; ?>
2+
13
@extends('layouts.sidebar')
24

35
@section('content')
@@ -117,6 +119,15 @@
117119
</svg>
118120
</span>
119121
</a>
122+
@php $adminUser = User::whereNotNull('auth_as')->where('role', 'admin')->first(); @endphp
123+
<a class="btn btn-sm btn-icon btn-primary" style="@if(!$adminUser) background:#3a57e8;border-color:#3a57e8; @else background:#6c757d;border-color:#6c757d; @endif" data-bs-toggle="tooltip" data-bs-placement="top" data-original-title="{{__('messages.tt.Impersonate')}}" @if(!$adminUser) href="{{ route('authAsID', $user->id ) }}" @endif aria-label="Impersonate" data-bs-original-title="Impersonate">
124+
<span class="btn-inner">
125+
<svg class="icon-20" width="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
126+
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.59151 15.2068C13.2805 15.2068 16.4335 15.7658 16.4335 17.9988C16.4335 20.2318 13.3015 20.8068 9.59151 20.8068C5.90151 20.8068 2.74951 20.2528 2.74951 18.0188C2.74951 15.7848 5.88051 15.2068 9.59151 15.2068Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
127+
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.59157 12.0198C7.16957 12.0198 5.20557 10.0568 5.20557 7.63476C5.20557 5.21276 7.16957 3.24976 9.59157 3.24976C12.0126 3.24976 13.9766 5.21276 13.9766 7.63476C13.9856 10.0478 12.0356 12.0108 9.62257 12.0198H9.59157Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M16.4829 10.8815C18.0839 10.6565 19.3169 9.28253 19.3199 7.61953C19.3199 5.98053 18.1249 4.62053 16.5579 4.36353" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M18.5952 14.7322C20.1462 14.9632 21.2292 15.5072 21.2292 16.6272C21.2292 17.3982 20.7192 17.8982 19.8952 18.2112" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
128+
</svg>
129+
</span>
130+
</a>
120131
<a class="btn btn-sm btn-icon btn-danger confirmation" data-bs-toggle="tooltip" data-bs-placement="top" data-original-title="{{__('messages.tt.Delete')}}" href="{{ route('deleteUser', ['id' => $user->id] ) }}" aria-label="Delete" data-bs-original-title="Delete">
121132
<span class="btn-inner">
122133
<svg class="icon-20" width="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">

routes/web.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696

9797
Route::get('/demo-page', [App\Http\Controllers\HomeController::class, 'demo'])->name('demo');
9898

99-
Route::middleware(['auth', 'blocked'])->group(function () {
99+
Route::middleware(['auth', 'blocked', 'impersonate'])->group(function () {
100100
//User route
101101
Route::group([
102102
'middleware' => env('REGISTER_AUTH'),
@@ -128,6 +128,7 @@
128128
Route::get('/clearIcon/{id}', [UserController::class, 'clearIcon'])->name('clearIcon');
129129
Route::get('/studio/page/delprofilepicture', [UserController::class, 'delProfilePicture'])->name('delProfilePicture');
130130
Route::get('/studio/delete-user/{id}', [UserController::class, 'deleteUser'])->name('deleteUser')->middleware('verified');
131+
Route::post('/auth-as', [AdminController::class, 'authAs'])->name('authAs');
131132
if(env('ALLOW_USER_EXPORT') != false){
132133
Route::get('/export-links', [UserController::class, 'exportLinks'])->name('exportLinks');
133134
Route::get('/export-all', [UserController::class, 'exportAll'])->name('exportAll');
@@ -144,7 +145,7 @@
144145
Route::get('/social-auth/{provider}/callback', [SocialLoginController::class, 'providerCallback']);
145146
Route::get('/social-auth/{provider}', [SocialLoginController::class, 'redirectToProvider'])->name('social.redirect');
146147

147-
Route::middleware(['auth', 'blocked'])->group(function () {
148+
Route::middleware(['auth', 'blocked', 'impersonate'])->group(function () {
148149
//Admin route
149150
Route::group([
150151
'middleware' => 'admin',
@@ -179,6 +180,7 @@
179180
Route::get('/admin/config', [AdminController::class, 'showConfig'])->name('showConfig');
180181
Route::post('/admin/config', [AdminController::class, 'editConfig'])->name('editConfig');
181182
Route::get('/send-test-email', [AdminController::class, 'SendTestMail'])->name('SendTestMail');
183+
Route::get('/auth-as/{id}', [AdminController::class, 'authAsID'])->name('authAsID');
182184
Route::get('/theme-updater', function () {return view('studio/theme-updater', []);});
183185
Route::get('/update', function () {return view('update', []);});
184186
Route::get('/backup', function () {return view('backup', []);});

0 commit comments

Comments
 (0)