diff --git a/app/Http/Controllers/Admin/ImpersonationController.php b/app/Http/Controllers/Admin/ImpersonationController.php index f076b9c..8f72753 100644 --- a/app/Http/Controllers/Admin/ImpersonationController.php +++ b/app/Http/Controllers/Admin/ImpersonationController.php @@ -12,7 +12,7 @@ class ImpersonationController extends Controller { public function __construct() { - $this->middleware('admin'); + $this->middleware('moderator'); } public function impersonate($identifier) { @@ -29,12 +29,33 @@ class ImpersonationController extends Controller } } - if (!$user) return $this->error([ - 'message'=> 'User not found.' + if (!$user) { + return $this->error([ + 'message'=> 'User not found.' + ]); + } else if ($user->admin) { + return $this->error([ + 'message' => 'You cannot impersonate an admin.', + ]); + } + + + \Log::warning('Impersonation started',[ + 'from_id' => auth()->id(), + 'from_email' => auth()->user()->email, + 'target_id' => $user->id, + 'target_email' => $user->id, ]); // Be this user - $token = auth()->login($user); + if (auth()->user()->moderator) { + $token = auth()->claims([ + 'impersonating' => true, + 'impersonator_id' => auth()->id(), + ])->login($user); + } else { + $token = auth()->login($user); + } return $this->success([ 'token' => $token ]); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index c2311a2..1f2eb9d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -5,7 +5,9 @@ namespace App\Http; use App\Http\Middleware\AcceptsJsonMiddleware; use App\Http\Middleware\AuthenticateJWT; use App\Http\Middleware\CustomDomainRestriction; +use App\Http\Middleware\ImpersonationMiddleware; use App\Http\Middleware\IsAdmin; +use App\Http\Middleware\IsModerator; use App\Http\Middleware\IsNotSubscribed; use App\Http\Middleware\IsSubscribed; use Illuminate\Foundation\Http\Kernel as HttpKernel; @@ -58,6 +60,7 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\EncryptCookies::class, \Illuminate\Session\Middleware\StartSession::class, + ImpersonationMiddleware::class, ], ]; @@ -72,6 +75,7 @@ class Kernel extends HttpKernel 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'admin' => IsAdmin::class, + 'moderator' => IsModerator::class, 'subscribed' => IsSubscribed::class, 'not-subscribed' => IsNotSubscribed::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, diff --git a/app/Http/Middleware/ImpersonationMiddleware.php b/app/Http/Middleware/ImpersonationMiddleware.php new file mode 100644 index 0000000..9399f24 --- /dev/null +++ b/app/Http/Middleware/ImpersonationMiddleware.php @@ -0,0 +1,94 @@ +check() || + !auth()->payload()->get('impersonating')) { + return $next($request); + } + // Check that route is allowed + $routeName = $request->route()->getName(); + if (!in_array($routeName, self::ALLOWED_ROUTES)) { + return response([ + 'message' => 'Unauthorized when impersonating', + 'route' => $routeName, + 'impersonator' => auth()->payload()->get('impersonator_id'), + 'impersonated_account' => auth()->id(), + 'url' => $request->fullUrl(), + 'payload' => $request->all() + ], 403); + } else if (in_array($routeName, self::LOG_ROUTES)) { + \Log::warning(self::ADMIN_LOG_PREFIX . 'Impersonator action', [ + 'route' => $routeName, + 'url' => $request->fullUrl(), + 'impersonated_account' => auth()->id(), + 'impersonator' => auth()->payload()->get('impersonator_id'), + 'payload' => $request->all() + ]); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/IsModerator.php b/app/Http/Middleware/IsModerator.php new file mode 100644 index 0000000..8c0150b --- /dev/null +++ b/app/Http/Middleware/IsModerator.php @@ -0,0 +1,32 @@ +user() && !$request->user()->moderator) { + // This user is not a paying customer... + if ($request->expectsJson()) { + return response([ + 'message' => 'You are not allowed.', + 'type' => 'error', + ], 403); + } + return redirect('home'); + } + + return $next($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 5e8de1d..411da8a 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -18,6 +18,7 @@ class UserResource extends JsonResource 'is_subscribed' => $this->is_subscribed, 'has_enterprise_subscription' => $this->has_enterprise_subscription, 'admin' => $this->admin, + 'moderator' => $this->moderator, 'template_editor' => $this->template_editor, 'has_customer_id' => $this->has_customer_id, 'has_forms' => $this->has_forms, diff --git a/app/Models/User.php b/app/Models/User.php index 0d826f5..317d37a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -102,6 +102,11 @@ class User extends Authenticatable implements JWTSubject return in_array($this->email, config('opnform.admin_emails')); } + public function getModeratorAttribute() + { + return in_array($this->email, config('opnform.moderator_emails')) || $this->admin; + } + public function getTemplateEditorAttribute() { return $this->admin || in_array($this->email, config('opnform.template_editor_emails')); diff --git a/client/components/global/Navbar.vue b/client/components/global/Navbar.vue index 3fc0d0e..8338e18 100644 --- a/client/components/global/Navbar.vue +++ b/client/components/global/Navbar.vue @@ -100,6 +100,15 @@ Settings + + + + + Admin + + { const authStore = useAuthStore() if (authStore.check && !authStore.user?.admin) { - console.log('redirecting to home') return navigateTo({ name: 'home' }) } }) diff --git a/client/middleware/moderator.js b/client/middleware/moderator.js new file mode 100644 index 0000000..d6822c5 --- /dev/null +++ b/client/middleware/moderator.js @@ -0,0 +1,6 @@ +export default defineNuxtRouteMiddleware((to, from) => { + const authStore = useAuthStore() + if (authStore.check && !authStore.user?.moderator) { + return navigateTo({ name: 'home' }) + } +}) diff --git a/client/pages/settings/admin.vue b/client/pages/settings/admin.vue index a6f0c33..fa6d1a2 100644 --- a/client/pages/settings/admin.vue +++ b/client/pages/settings/admin.vue @@ -40,7 +40,7 @@ import {opnFetch} from "~/composables/useOpnApi.js"; import {fetchAllWorkspaces} from "~/stores/workspaces.js"; definePageMeta({ - middleware: "admin" + middleware: "moderator" }) useOpnSeoMeta({ diff --git a/config/opnform.php b/config/opnform.php index 92ce264..2bf9ff0 100644 --- a/config/opnform.php +++ b/config/opnform.php @@ -2,6 +2,7 @@ return [ 'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? ''), + 'moderator_emails' => explode(",", env('MODERATOR_EMAILS') ?? ''), 'template_editor_emails' => explode(",", env('TEMPLATE_EDITOR_EMAILS') ?? ''), 'extra_pro_users_emails' => explode(",", env('EXTRA_PRO_USERS_EMAILS') ?? ''), ]; diff --git a/routes/api.php b/routes/api.php index 589a09d..b381f5d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -37,7 +37,7 @@ use Illuminate\Support\Facades\Route; Route::group(['middleware' => 'auth:api'], function () { Route::post('logout', [LoginController::class, 'logout']); - Route::get('user', [UserController::class, 'current']); + Route::get('user', [UserController::class, 'current'])->name('user.current'); Route::delete('user', [UserController::class, 'deleteAccount']); Route::patch('settings/profile', [ProfileController::class, 'update']);