Compare commits
2 Commits
643aafe07d
...
a2e3ad20e7
| Author | SHA1 | Date | |
|---|---|---|---|
| a2e3ad20e7 | |||
| 7f5e0fadf7 |
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\App;
|
use App\Models\App;
|
||||||
use App\Models\AppRestriction;
|
use App\Models\Role;
|
||||||
|
use App\Models\UserAppOverride;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
@ -17,12 +18,17 @@ public function index()
|
|||||||
{
|
{
|
||||||
$admin = Auth::user();
|
$admin = Auth::user();
|
||||||
|
|
||||||
// Fetch all users to display on the dashboard for administration
|
// Fetch all users with their roles and overrides
|
||||||
$users = User::orderBy('created_at', 'desc')->get();
|
$users = User::with(['roles', 'overrides.app'])->orderBy('created_at', 'desc')->get();
|
||||||
|
|
||||||
// Fetch all apps and active restrictions
|
// Fetch all apps
|
||||||
$apps = App::orderBy('name', 'asc')->get();
|
$apps = App::orderBy('name', 'asc')->get();
|
||||||
$restrictions = AppRestriction::with('app')->orderBy('created_at', 'desc')->get();
|
|
||||||
|
// Fetch all roles
|
||||||
|
$roles = Role::with('apps')->orderBy('name', 'asc')->get();
|
||||||
|
|
||||||
|
// Fetch all overrides
|
||||||
|
$overrides = UserAppOverride::with(['user', 'app'])->orderBy('created_at', 'desc')->get();
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
$stats = [
|
$stats = [
|
||||||
@ -30,9 +36,10 @@ public function index()
|
|||||||
'admins_count' => User::where('role', 'admin')->count(),
|
'admins_count' => User::where('role', 'admin')->count(),
|
||||||
'blocked_count' => User::where('is_blocked', true)->count(),
|
'blocked_count' => User::where('is_blocked', true)->count(),
|
||||||
'apps_count' => App::count(),
|
'apps_count' => App::count(),
|
||||||
|
'roles_count' => Role::count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('admin.dashboard', compact('admin', 'users', 'apps', 'restrictions', 'stats'));
|
return view('admin.dashboard', compact('admin', 'users', 'apps', 'roles', 'overrides', 'stats'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,41 +80,121 @@ public function destroyApp($id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restrict an app for a specific user email.
|
* Store a new role.
|
||||||
*/
|
*/
|
||||||
public function storeRestriction(Request $request)
|
public function storeRole(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'email' => 'required|email|max:255',
|
'name' => 'required|string|max:255|unique:roles,name',
|
||||||
'app_id' => 'required|exists:apps,id',
|
'description' => 'nullable|string',
|
||||||
|
'apps' => 'nullable|array',
|
||||||
|
'apps.*' => 'exists:apps,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Prevent duplicate restrictions
|
$role = Role::create([
|
||||||
$exists = AppRestriction::where('email', $request->email)
|
'name' => $request->name,
|
||||||
|
'description' => $request->description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->has('apps')) {
|
||||||
|
$role->apps()->sync($request->apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.dashboard')->with('success', 'Role created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing role.
|
||||||
|
*/
|
||||||
|
public function updateRole(Request $request, $id)
|
||||||
|
{
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255|unique:roles,name,' . $role->id,
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
'apps' => 'nullable|array',
|
||||||
|
'apps.*' => 'exists:apps,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role->update([
|
||||||
|
'name' => $request->name,
|
||||||
|
'description' => $request->description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role->apps()->sync($request->apps ?? []);
|
||||||
|
|
||||||
|
return redirect()->route('admin.dashboard')->with('success', 'Role updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a role.
|
||||||
|
*/
|
||||||
|
public function destroyRole($id)
|
||||||
|
{
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
$role->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.dashboard')->with('success', 'Role deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user roles.
|
||||||
|
*/
|
||||||
|
public function updateUserRoles(Request $request, $id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'roles' => 'nullable|array',
|
||||||
|
'roles.*' => 'exists:roles,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->roles()->sync($request->roles ?? []);
|
||||||
|
|
||||||
|
return redirect()->route('admin.dashboard')->with('success', 'User roles updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a user-specific app override.
|
||||||
|
*/
|
||||||
|
public function storeUserOverride(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'user_id' => 'required|exists:users,id',
|
||||||
|
'app_id' => 'required|exists:apps,id',
|
||||||
|
'type' => 'required|in:allow,deny',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::findOrFail($request->user_id);
|
||||||
|
|
||||||
|
// Prevent duplicate overrides
|
||||||
|
$exists = UserAppOverride::where('user_id', $user->id)
|
||||||
->where('app_id', $request->app_id)
|
->where('app_id', $request->app_id)
|
||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
return redirect()->route('admin.dashboard')->with('error', 'This user is already restricted from accessing this service.');
|
return redirect()->route('admin.dashboard')->with('error', 'An override for this service and user already exists.');
|
||||||
}
|
}
|
||||||
|
|
||||||
AppRestriction::create([
|
UserAppOverride::create([
|
||||||
'email' => $request->email,
|
'user_id' => $user->id,
|
||||||
'app_id' => $request->app_id,
|
'app_id' => $request->app_id,
|
||||||
|
'type' => $request->type,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.dashboard')->with('success', 'Access restriction created successfully.');
|
return redirect()->route('admin.dashboard')->with('success', 'User service override registered successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an app restriction.
|
* Remove a user-specific override.
|
||||||
*/
|
*/
|
||||||
public function destroyRestriction($id)
|
public function destroyUserOverride($id)
|
||||||
{
|
{
|
||||||
$restriction = AppRestriction::findOrFail($id);
|
$override = UserAppOverride::findOrFail($id);
|
||||||
$restriction->delete();
|
$override->delete();
|
||||||
|
|
||||||
return redirect()->route('admin.dashboard')->with('success', 'Access restriction removed successfully.');
|
return redirect()->route('admin.dashboard')->with('success', 'User service override removed successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -14,14 +16,94 @@ public function index()
|
|||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Get all apps that are NOT restricted for this user's email
|
// Get authorized apps for this user
|
||||||
$services = \App\Models\App::whereNotExists(function ($query) use ($user) {
|
$services = $user->authorizedApps();
|
||||||
$query->select(\DB::raw(1))
|
|
||||||
->from('app_restrictions')
|
|
||||||
->whereColumn('app_restrictions.app_id', 'apps.id')
|
|
||||||
->where('app_restrictions.email', $user->email);
|
|
||||||
})->orderBy('name', 'asc')->get();
|
|
||||||
|
|
||||||
return view('dashboard', compact('user', 'services'));
|
return view('dashboard', compact('user', 'services'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch a service app using SSO.
|
||||||
|
*/
|
||||||
|
public function launchSSO($id)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$app = \App\Models\App::findOrFail($id);
|
||||||
|
|
||||||
|
// Check if user is authorized to access this app
|
||||||
|
$authorizedApps = $user->authorizedApps();
|
||||||
|
if (!$authorizedApps->contains('id', $app->id)) {
|
||||||
|
return redirect()->route('dashboard')->with('error', 'Access denied. You do not have permission to access this service.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it is a Microsoft service
|
||||||
|
$isMicrosoft = str_contains($app->url, 'microsoft.com') ||
|
||||||
|
str_contains($app->url, 'office.com') ||
|
||||||
|
str_contains($app->url, 'office365.com') ||
|
||||||
|
str_contains($app->url, 'outlook.com') ||
|
||||||
|
$app->icon_svg === 'outlook' ||
|
||||||
|
$app->icon_svg === 'teams';
|
||||||
|
|
||||||
|
if ($isMicrosoft && $user->microsoft_id) {
|
||||||
|
// Save target app URL in session so callback knows where to redirect
|
||||||
|
session(['sso_target_url' => $app->url]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine domain hint based on user's email domain to skip account type prompt
|
||||||
|
$domainHint = (str_ends_with($user->email, '.onmicrosoft.com') || (str_contains($user->email, '@') && !str_ends_with($user->email, 'outlook.com') && !str_ends_with($user->email, 'hotmail.com') && !str_ends_with($user->email, 'live.com')))
|
||||||
|
? 'organizations'
|
||||||
|
: 'consumers';
|
||||||
|
|
||||||
|
// Redirect to Microsoft OAuth authorize page with login_hint, domain_hint and custom callback
|
||||||
|
return Socialite::driver('microsoft')
|
||||||
|
->redirectUrl(route('sso.callback'))
|
||||||
|
->with([
|
||||||
|
'login_hint' => $user->email,
|
||||||
|
'domain_hint' => $domainHint
|
||||||
|
])
|
||||||
|
->redirect();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Microsoft SSO launch failed', [
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
// Fallback: direct redirect
|
||||||
|
return redirect($app->url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-Microsoft or non-Microsoft-linked users, redirect directly
|
||||||
|
return redirect($app->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the callback for Microsoft SSO.
|
||||||
|
*/
|
||||||
|
public function handleSSOCallback(Request $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$microsoftUser = Socialite::driver('microsoft')
|
||||||
|
->redirectUrl(route('sso.callback'))
|
||||||
|
->user();
|
||||||
|
|
||||||
|
if ($microsoftUser && $user) {
|
||||||
|
// Update tokens in db
|
||||||
|
$user->update([
|
||||||
|
'microsoft_token' => $microsoftUser->token,
|
||||||
|
'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Microsoft SSO callback failed', [
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
// If callback fails but we have target URL, we can still try to redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetUrl = session('sso_target_url', 'https://outlook.office.com/mail/');
|
||||||
|
session()->forget('sso_target_url');
|
||||||
|
|
||||||
|
return redirect($targetUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,9 @@ public function handle(Request $request, Closure $next, string $role): Response
|
|||||||
|
|
||||||
if (auth()->user()->role !== $role) {
|
if (auth()->user()->role !== $role) {
|
||||||
if (auth()->user()->role === 'admin') {
|
if (auth()->user()->role === 'admin') {
|
||||||
|
if ($role === 'user') {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
return redirect()->route('admin.dashboard');
|
return redirect()->route('admin.dashboard');
|
||||||
}
|
}
|
||||||
return redirect()->route('dashboard')->with('error', 'Unauthorized access.');
|
return redirect()->route('dashboard')->with('error', 'Unauthorized access.');
|
||||||
|
|||||||
29
app/Models/Role.php
Normal file
29
app/Models/Role.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'description'])]
|
||||||
|
class Role extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the users assigned to this role.
|
||||||
|
*/
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'role_user');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the apps/services assigned to this role.
|
||||||
|
*/
|
||||||
|
public function apps()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(App::class, 'app_role', 'role_id', 'app_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,6 +25,53 @@ public function isAdmin(): bool
|
|||||||
return $this->role === 'admin';
|
return $this->role === 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the roles assigned to this user.
|
||||||
|
*/
|
||||||
|
public function roles()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Role::class, 'role_user');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the app overrides (allow/deny) for this user.
|
||||||
|
*/
|
||||||
|
public function overrides()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserAppOverride::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authorized apps for this user, computed from roles and overrides.
|
||||||
|
*/
|
||||||
|
public function authorizedApps()
|
||||||
|
{
|
||||||
|
if ($this->isAdmin()) {
|
||||||
|
return App::orderBy('name', 'asc')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleIds = $this->roles()->pluck('roles.id')->toArray();
|
||||||
|
|
||||||
|
// Get apps associated with user's roles
|
||||||
|
$roleAppIds = \DB::table('app_role')
|
||||||
|
->whereIn('role_id', $roleIds)
|
||||||
|
->pluck('app_id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// Get user overrides
|
||||||
|
$overrides = $this->overrides()->get();
|
||||||
|
$allowedAppIds = $overrides->where('type', 'allow')->pluck('app_id')->toArray();
|
||||||
|
$deniedAppIds = $overrides->where('type', 'deny')->pluck('app_id')->toArray();
|
||||||
|
|
||||||
|
// Compute final allowed app IDs: (Role Apps + Allowed Overrides) - Denied Overrides
|
||||||
|
$finalAppIds = array_diff(
|
||||||
|
array_unique(array_merge($roleAppIds, $allowedAppIds)),
|
||||||
|
$deniedAppIds
|
||||||
|
);
|
||||||
|
|
||||||
|
return App::whereIn('id', $finalAppIds)->orderBy('name', 'asc')->get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes that should be cast.
|
* Get the attributes that should be cast.
|
||||||
*
|
*
|
||||||
|
|||||||
29
app/Models/UserAppOverride.php
Normal file
29
app/Models/UserAppOverride.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
|
||||||
|
#[Fillable(['user_id', 'app_id', 'type'])]
|
||||||
|
class UserAppOverride extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user this override belongs to.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the app/service being overridden.
|
||||||
|
*/
|
||||||
|
public function app()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(App::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// 1. Create roles table
|
||||||
|
Schema::create('roles', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create role_user pivot table for user roles (many-to-many)
|
||||||
|
Schema::create('role_user', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
|
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
|
||||||
|
$table->unique(['user_id', 'role_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create app_role pivot table for role services (many-to-many)
|
||||||
|
Schema::create('app_role', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
|
||||||
|
$table->foreignId('app_id')->constrained('apps')->onDelete('cascade');
|
||||||
|
$table->unique(['role_id', 'app_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Create user_app_overrides table (for user-specific inclusions/exclusions)
|
||||||
|
Schema::create('user_app_overrides', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
|
$table->foreignId('app_id')->constrained('apps')->onDelete('cascade');
|
||||||
|
$table->enum('type', ['allow', 'deny']); // allow = add extra service, deny = revoke service
|
||||||
|
$table->unique(['user_id', 'app_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Drop old app_restrictions table if it exists
|
||||||
|
Schema::dropIfExists('app_restrictions');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_app_overrides');
|
||||||
|
Schema::dropIfExists('app_role');
|
||||||
|
Schema::dropIfExists('role_user');
|
||||||
|
Schema::dropIfExists('roles');
|
||||||
|
|
||||||
|
// Recreate the old app_restrictions table if we roll back
|
||||||
|
Schema::create('app_restrictions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('email');
|
||||||
|
$table->foreignId('app_id')->constrained('apps')->onDelete('cascade');
|
||||||
|
$table->unique(['email', 'app_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -3,9 +3,11 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\App;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\UserAppOverride;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
@ -17,8 +19,8 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Seed default administrator
|
// 1. Seed default administrator
|
||||||
User::updateOrCreate(
|
$admin = User::updateOrCreate(
|
||||||
['email' => 'admin@company.com'],
|
['email' => 'admin@company.com'],
|
||||||
[
|
[
|
||||||
'name' => 'System Administrator',
|
'name' => 'System Administrator',
|
||||||
@ -27,18 +29,8 @@ public function run(): void
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Seed default standard user (without password since users use Microsoft OAuth)
|
// 2. Seed default apps
|
||||||
User::updateOrCreate(
|
$outlook = App::updateOrCreate(
|
||||||
['email' => 'user@company.com'],
|
|
||||||
[
|
|
||||||
'name' => 'John Doe User',
|
|
||||||
'role' => 'user',
|
|
||||||
'microsoft_id' => 'mock-microsoft-id-123',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Seed default apps
|
|
||||||
\App\Models\App::updateOrCreate(
|
|
||||||
['name' => 'Outlook'],
|
['name' => 'Outlook'],
|
||||||
[
|
[
|
||||||
'url' => 'https://outlook.office.com/mail/',
|
'url' => 'https://outlook.office.com/mail/',
|
||||||
@ -49,7 +41,7 @@ public function run(): void
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
\App\Models\App::updateOrCreate(
|
$teams = App::updateOrCreate(
|
||||||
['name' => 'Microsoft Teams'],
|
['name' => 'Microsoft Teams'],
|
||||||
[
|
[
|
||||||
'url' => 'https://teams.microsoft.com/v2/',
|
'url' => 'https://teams.microsoft.com/v2/',
|
||||||
@ -60,7 +52,7 @@ public function run(): void
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
\App\Models\App::updateOrCreate(
|
$keka = App::updateOrCreate(
|
||||||
['name' => 'Keka HR'],
|
['name' => 'Keka HR'],
|
||||||
[
|
[
|
||||||
'url' => 'https://sentientgeeks.keka.com/',
|
'url' => 'https://sentientgeeks.keka.com/',
|
||||||
@ -70,5 +62,92 @@ public function run(): void
|
|||||||
'tag' => 'HR Portal'
|
'tag' => 'HR Portal'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. Seed default roles
|
||||||
|
$pmRole = Role::updateOrCreate(
|
||||||
|
['name' => 'Project Manager'],
|
||||||
|
['description' => 'Oversees projects, budgets, and milestones.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$leadRole = Role::updateOrCreate(
|
||||||
|
['name' => 'Team Lead'],
|
||||||
|
['description' => 'Manages dev teams and assigns work items.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$salesRole = Role::updateOrCreate(
|
||||||
|
['name' => 'Sales Team'],
|
||||||
|
['description' => 'Drives client acquisition and manages CRM leads.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$devRole = Role::updateOrCreate(
|
||||||
|
['name' => 'Developer'],
|
||||||
|
['description' => 'Writes code and builds features.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$internRole = Role::updateOrCreate(
|
||||||
|
['name' => 'Intern'],
|
||||||
|
['description' => 'Learning and assisting on project modules.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$hrRole = Role::updateOrCreate(
|
||||||
|
['name' => 'HR Manager'],
|
||||||
|
['description' => 'Handles recruitment, payroll, and employee relations.']
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Assign apps/services to specific roles
|
||||||
|
// HR has access to Outlook, Teams, and Keka
|
||||||
|
$hrRole->apps()->sync([$outlook->id, $teams->id, $keka->id]);
|
||||||
|
|
||||||
|
// Developers have access to Outlook and Teams
|
||||||
|
$devRole->apps()->sync([$outlook->id, $teams->id]);
|
||||||
|
|
||||||
|
// Team Leads have access to Outlook and Teams
|
||||||
|
$leadRole->apps()->sync([$outlook->id, $teams->id]);
|
||||||
|
|
||||||
|
// Project Managers have access to Outlook and Teams
|
||||||
|
$pmRole->apps()->sync([$outlook->id, $teams->id]);
|
||||||
|
|
||||||
|
// Sales Team has access to Teams
|
||||||
|
$salesRole->apps()->sync([$teams->id]);
|
||||||
|
|
||||||
|
// Interns have access to Teams (no Outlook, no Keka)
|
||||||
|
$internRole->apps()->sync([$teams->id]);
|
||||||
|
|
||||||
|
// 5. Seed default standard users and assign roles
|
||||||
|
// John Doe (Developer & Team Lead) - Test multiple roles
|
||||||
|
$user1 = User::updateOrCreate(
|
||||||
|
['email' => 'user@company.com'],
|
||||||
|
[
|
||||||
|
'name' => 'John Doe User',
|
||||||
|
'role' => 'user',
|
||||||
|
'microsoft_id' => 'mock-microsoft-id-123',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$user1->roles()->sync([$devRole->id, $leadRole->id]);
|
||||||
|
|
||||||
|
// Jane Smith (HR Manager)
|
||||||
|
$user2 = User::updateOrCreate(
|
||||||
|
['email' => 'hr@company.com'],
|
||||||
|
[
|
||||||
|
'name' => 'Jane Smith HR',
|
||||||
|
'role' => 'user',
|
||||||
|
'microsoft_id' => 'mock-microsoft-id-456',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$user2->roles()->sync([$hrRole->id]);
|
||||||
|
|
||||||
|
// 6. Seed user-specific overrides for testing
|
||||||
|
// John Doe (Developer & Team Lead - has Outlook and Teams via roles)
|
||||||
|
// Scenario A: Revoke Outlook for John Doe (even though Developer role grants it)
|
||||||
|
UserAppOverride::updateOrCreate(
|
||||||
|
['user_id' => $user1->id, 'app_id' => $outlook->id],
|
||||||
|
['type' => 'deny']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Scenario B: Add Keka HR as an extra service for John Doe (irrespective of Developer role)
|
||||||
|
UserAppOverride::updateOrCreate(
|
||||||
|
['user_id' => $user1->id, 'app_id' => $keka->id],
|
||||||
|
['type' => 'allow']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/Screenshot_2.png
Normal file
BIN
public/Screenshot_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 473 KiB |
@ -368,6 +368,113 @@
|
|||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Backdrop */
|
||||||
|
.admin-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(11, 15, 25, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.admin-modal.active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.admin-modal-content {
|
||||||
|
background-color: rgba(25, 30, 45, 0.95);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
||||||
|
}
|
||||||
|
.admin-modal.active .admin-modal-content {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--card-border);
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #a5b4fc;
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.modal-close:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border-top: 1px solid var(--card-border);
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.btn-cancel {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-cancel:hover {
|
||||||
|
color: white;
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.checkbox-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.checkbox-item input {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -436,6 +543,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-icon" style="color: #fbbf24;">📦</div>
|
<div class="stat-icon" style="color: #fbbf24;">📦</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div>
|
||||||
|
<div class="stat-label">System Roles</div>
|
||||||
|
<div class="stat-value">{{ $stats['roles_count'] }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon" style="color: #a5b4fc;">🏷️</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Alert messages in dashboard -->
|
<!-- Alert messages in dashboard -->
|
||||||
@ -453,8 +568,8 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- App Management & Access Restrictions Grid -->
|
<!-- App Management, Roles Management, & User Overrides Grid -->
|
||||||
<section class="management-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 20px; margin-bottom: 40px;">
|
<section class="management-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 20px; margin-bottom: 40px;">
|
||||||
|
|
||||||
<!-- App Management Card -->
|
<!-- App Management Card -->
|
||||||
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
||||||
@ -491,7 +606,7 @@
|
|||||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Service Description</label>
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Service Description</label>
|
||||||
<input type="text" name="desc" placeholder="Brief description of the service..." class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
<input type="text" name="desc" placeholder="Brief description of the service..." class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; width:100%; font-size:0.85rem; border-radius:8px; padding:10px;">+ Register Service App</button>
|
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; width:100%; font-size:0.85rem; border-radius:8px; padding:10px; border:none; cursor:pointer; font-weight:600;">+ Register Service App</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Registered Apps List -->
|
<!-- Registered Apps List -->
|
||||||
@ -502,7 +617,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Service</th>
|
<th>Service</th>
|
||||||
<th>Tag</th>
|
<th>Tag</th>
|
||||||
<th>URL</th>
|
|
||||||
<th style="text-align: right;">Action</th>
|
<th style="text-align: right;">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -516,9 +630,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><span class="badge" style="background-color:rgba(255,255,255,0.05); color:#cbd5e1; border:1px solid rgba(255,255,255,0.1);">{{ $app->tag }}</span></td>
|
<td><span class="badge" style="background-color:rgba(255,255,255,0.05); color:#cbd5e1; border:1px solid rgba(255,255,255,0.1);">{{ $app->tag }}</span></td>
|
||||||
<td><span style="color:var(--text-muted); display:inline-block; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ $app->url }}</span></td>
|
|
||||||
<td style="text-align: right;">
|
<td style="text-align: right;">
|
||||||
<form action="{{ route('admin.apps.destroy', $app->id) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this service? All access restrictions for it will be removed.')" style="display:inline;">
|
<form action="{{ route('admin.apps.destroy', $app->id) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this service?')" style="display:inline;">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit" class="action-btn" style="color:#ef4444; border-color:rgba(239,68,68,0.2); font-size:0.75rem; padding:4px 8px;">Delete</button>
|
<button type="submit" class="action-btn" style="color:#ef4444; border-color:rgba(239,68,68,0.2); font-size:0.75rem; padding:4px 8px;">Delete</button>
|
||||||
@ -527,7 +640,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" style="text-align: center; color: var(--text-muted); padding: 20px;">No registered service apps.</td>
|
<td colspan="3" style="text-align: center; color: var(--text-muted); padding: 20px;">No registered service apps.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -535,65 +648,173 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Access Restrictions Card -->
|
<!-- Role Management Card -->
|
||||||
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
||||||
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">Restrict User Access</h3>
|
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">Manage Roles & Services</h3>
|
||||||
|
|
||||||
<!-- Add Restriction Form -->
|
<!-- Add Role Form -->
|
||||||
<form action="{{ route('admin.restrictions.store') }}" method="POST" style="margin-bottom: 30px;">
|
<form action="{{ route('admin.roles.store') }}" method="POST" style="margin-bottom: 30px;">
|
||||||
@csrf
|
@csrf
|
||||||
<div style="margin-bottom: 12px;">
|
<div style="margin-bottom: 12px;">
|
||||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">User Email Address</label>
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Role Name</label>
|
||||||
<input type="email" name="email" required placeholder="user@sentientgeeks.com" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
<input type="text" name="name" required placeholder="e.g. Developer, HR" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Role Description</label>
|
||||||
|
<input type="text" name="description" placeholder="Description of this role..." class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="margin-bottom: 16px;">
|
||||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Restrict Service App</label>
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Assign Services Access</label>
|
||||||
<select name="app_id" required class="form-input" style="width:100%; padding:10px; background:rgba(11,15,25,0.9); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
<div class="checkbox-grid">
|
||||||
<option value="">-- Select App to Restrict --</option>
|
|
||||||
@foreach($apps as $app)
|
@foreach($apps as $app)
|
||||||
<option value="{{ $app->id }}">{{ $app->name }}</option>
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" name="apps[]" value="{{ $app->id }}">
|
||||||
|
{{ $app->name }}
|
||||||
|
</label>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" style="background-color: #fbbf24; color: #0b0f19; width:100%; font-size:0.85rem; border-radius:8px; padding:10px; font-weight:700;">Restrict Access</button>
|
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; width:100%; font-size:0.85rem; border-radius:8px; padding:10px; border:none; cursor:pointer; font-weight:600;">+ Create Role</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Active Restrictions List -->
|
<!-- Registered Roles List -->
|
||||||
<h4 style="font-size:0.9rem; font-weight:600; margin-bottom:12px; border-top:1px solid var(--card-border); padding-top:20px; color:#9ca3af;">Active Access Restrictions</h4>
|
<h4 style="font-size:0.9rem; font-weight:600; margin-bottom:12px; border-top:1px solid var(--card-border); padding-top:20px; color:#9ca3af;">System Application Roles</h4>
|
||||||
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
|
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
|
||||||
<table class="users-table" style="font-size:0.8rem;">
|
<table class="users-table" style="font-size:0.8rem;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User Email</th>
|
<th>Role Name</th>
|
||||||
<th>Restricted App</th>
|
<th>Assigned Services</th>
|
||||||
<th style="text-align: right;">Action</th>
|
<th style="text-align: right;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse($restrictions as $res)
|
@forelse($roles as $role)
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong style="color:#fca5a5;">{{ $res->email }}</strong></td>
|
|
||||||
<td>
|
<td>
|
||||||
@if($res->app)
|
<div>
|
||||||
<span style="display:flex; align-items:center; gap:6px;">
|
<strong>{{ $role->name }}</strong>
|
||||||
<div style="width:8px; height:8px; border-radius:50%; background-color:{{ $res->app->color }};"></div>
|
@if($role->description)
|
||||||
{{ $res->app->name }}
|
<div style="font-size:0.7rem; color:var(--text-muted); margin-top:2px;">{{ $role->description }}</div>
|
||||||
</span>
|
@endif
|
||||||
@else
|
</div>
|
||||||
<span style="color:#ef4444;">App Deleted</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right;">
|
<td>
|
||||||
<form action="{{ route('admin.restrictions.destroy', $res->id) }}" method="POST" style="display:inline;">
|
<div style="display:flex; flex-wrap:wrap; gap:4px;">
|
||||||
|
@forelse($role->apps as $app)
|
||||||
|
<span class="badge" style="background-color:{{ $app->color }}20; color:{{ $app->color }}; border:1px solid {{ $app->color }}40; font-size:0.65rem;">{{ $app->name }}</span>
|
||||||
|
@empty
|
||||||
|
<span style="color:var(--text-muted); font-size:0.7rem;">None</span>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
|
<button class="action-btn" onclick="openEditRoleModal({{ $role->id }}, '{{ addslashes($role->name) }}', '{{ addslashes($role->description) }}', {{ json_encode($role->apps->pluck('id')) }})" style="color:var(--info-color); border-color:rgba(6,182,212,0.2); font-size:0.75rem; padding:4px 8px; margin-right:4px;">Edit</button>
|
||||||
|
<form action="{{ route('admin.roles.destroy', $role->id) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this role?')" style="display:inline;">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit" class="action-btn" style="color:#fbbf24; border-color:rgba(245,158,11,0.2); font-size:0.75rem; padding:4px 8px;">Remove</button>
|
<button type="submit" class="action-btn" style="color:#ef4444; border-color:rgba(239,68,68,0.2); font-size:0.75rem; padding:4px 8px;">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align: center; color: var(--text-muted); padding: 20px;">No active restrictions. All apps visible to all users.</td>
|
<td colspan="3" style="text-align: center; color: var(--text-muted); padding: 20px;">No roles defined.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Service Overrides Card -->
|
||||||
|
<div class="admin-card" style="background-color: var(--card-bg); border: 1px solid var(--card-border); border-radius: 20px; padding: 30px; backdrop-filter: blur(16px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);">
|
||||||
|
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">User Service Overrides</h3>
|
||||||
|
|
||||||
|
<!-- Add Override Form -->
|
||||||
|
<form action="{{ route('admin.overrides.store') }}" method="POST" style="margin-bottom: 30px;">
|
||||||
|
@csrf
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Select User Account</label>
|
||||||
|
<select name="user_id" required class="form-input" style="width:100%; padding:10px; background:rgba(11,15,25,0.9); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
<option value="">-- Choose User --</option>
|
||||||
|
@foreach($users as $usr)
|
||||||
|
@if(!$usr->isAdmin())
|
||||||
|
<option value="{{ $usr->id }}">{{ $usr->name }} ({{ $usr->email }})</option>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;">
|
||||||
|
<div>
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Service App</label>
|
||||||
|
<select name="app_id" required class="form-input" style="width:100%; padding:10px; background:rgba(11,15,25,0.9); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
<option value="">-- Choose App --</option>
|
||||||
|
@foreach($apps as $app)
|
||||||
|
<option value="{{ $app->id }}">{{ $app->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Override Mode</label>
|
||||||
|
<select name="type" required class="form-input" style="width:100%; padding:10px; background:rgba(11,15,25,0.9); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
<option value="allow">ALLOW (Add extra service)</option>
|
||||||
|
<option value="deny">DENY (Revoke service)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn" style="background-color: #fbbf24; color: #0b0f19; width:100%; font-size:0.85rem; border-radius:8px; padding:10px; font-weight:700; border:none; cursor:pointer;">Apply Override</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Active Overrides List -->
|
||||||
|
<h4 style="font-size:0.9rem; font-weight:600; margin-bottom:12px; border-top:1px solid var(--card-border); padding-top:20px; color:#9ca3af;">Active Service Overrides</h4>
|
||||||
|
<div class="table-responsive" style="max-height: 250px; overflow-y: auto;">
|
||||||
|
<table class="users-table" style="font-size:0.8rem;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>App</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th style="text-align: right;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($overrides as $override)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<strong>{{ $override->user->name }}</strong>
|
||||||
|
<div style="font-size:0.7rem; color:var(--text-muted);">{{ $override->user->email }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($override->app)
|
||||||
|
<span style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<div style="width:8px; height:8px; border-radius:50%; background-color:{{ $override->app->color }};"></div>
|
||||||
|
{{ $override->app->name }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span style="color:#ef4444;">App Deleted</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($override->type === 'allow')
|
||||||
|
<span class="badge" style="background-color:rgba(16,185,129,0.12); color:#10b981; border:1px solid rgba(16,185,129,0.3); font-size:0.65rem; padding:2px 6px;">ADDITIONAL</span>
|
||||||
|
@else
|
||||||
|
<span class="badge" style="background-color:rgba(239,68,68,0.12); color:#ef4444; border:1px solid rgba(239,68,68,0.3); font-size:0.65rem; padding:2px 6px;">REVOKED</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right;">
|
||||||
|
<form action="{{ route('admin.overrides.destroy', $override->id) }}" method="POST" style="display:inline;">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="action-btn" style="color:#ef4444; border-color:rgba(239,68,68,0.2); font-size:0.75rem; padding:4px 8px;">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" style="text-align: center; color: var(--text-muted); padding: 20px;">No custom overrides active. Services resolved by roles.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -652,7 +873,14 @@
|
|||||||
@if($usr->role === 'admin')
|
@if($usr->role === 'admin')
|
||||||
<span class="badge badge-admin">Admin</span>
|
<span class="badge badge-admin">Admin</span>
|
||||||
@else
|
@else
|
||||||
<span class="badge badge-user">User</span>
|
<span class="badge badge-user">User Portal</span>
|
||||||
|
@endif
|
||||||
|
@if($usr->roles->isNotEmpty())
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:4px; margin-top:6px;">
|
||||||
|
@foreach($usr->roles as $role)
|
||||||
|
<span class="badge" style="background-color:rgba(99,102,241,0.1); color:#818cf8; border:1px solid rgba(99,102,241,0.25); font-size:0.65rem;">{{ $role->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -675,14 +903,17 @@
|
|||||||
{{ $usr->created_at ? $usr->created_at->format('Y-m-d H:i') : 'N/A' }}
|
{{ $usr->created_at ? $usr->created_at->format('Y-m-d H:i') : 'N/A' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right;">
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
@if($usr->id !== $admin->id)
|
@if($usr->id !== $admin->id)
|
||||||
|
@if(!$usr->isAdmin())
|
||||||
|
<button class="action-btn" onclick="openAssignRolesModal({{ $usr->id }}, '{{ addslashes($usr->name) }}', {{ json_encode($usr->roles->pluck('id')) }})" style="color:var(--accent-primary); border-color:rgba(99,102,241,0.2); font-size:0.75rem; padding:6px 12px; margin-right:4px;">Assign Roles</button>
|
||||||
|
@endif
|
||||||
<form action="{{ route('admin.users.toggle-block', $usr->id) }}" method="POST" style="display:inline;">
|
<form action="{{ route('admin.users.toggle-block', $usr->id) }}" method="POST" style="display:inline;">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="action-btn"
|
<button type="submit" class="action-btn"
|
||||||
style="font-size:0.75rem; padding:6px 12px; border-radius:6px; font-weight:600; cursor:pointer; transition:all 0.2s ease;
|
style="font-size:0.75rem; padding:6px 12px; border-radius:6px; font-weight:600; cursor:pointer; transition:all 0.2s ease;
|
||||||
{{ $usr->is_blocked ? 'color:#10b981; border-color:rgba(16,185,129,0.2); background-color:rgba(16,185,129,0.05);' : 'color:#ef4444; border-color:rgba(239,68,68,0.2); background-color:rgba(239,68,68,0.05);' }}">
|
{{ $usr->is_blocked ? 'color:#10b981; border-color:rgba(16,185,129,0.2); background-color:rgba(16,185,129,0.05);' : 'color:#ef4444; border-color:rgba(239,68,68,0.2); background-color:rgba(239,68,68,0.05);' }}">
|
||||||
{{ $usr->is_blocked ? 'Unblock User' : 'Block User' }}
|
{{ $usr->is_blocked ? 'Unblock' : 'Block' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
@ -704,6 +935,114 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Edit Role Modal -->
|
||||||
|
<div id="edit-role-modal" class="admin-modal">
|
||||||
|
<div class="admin-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Edit Application Role</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('edit-role-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="edit-role-form" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Role Name</label>
|
||||||
|
<input type="text" id="edit-role-name" name="name" required class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Description</label>
|
||||||
|
<input type="text" id="edit-role-description" name="description" class="form-input" style="width:100%; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Services Access</label>
|
||||||
|
<div class="checkbox-grid">
|
||||||
|
@foreach($apps as $app)
|
||||||
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" name="apps[]" value="{{ $app->id }}" class="edit-role-app-checkbox" id="edit-role-app-{{ $app->id }}">
|
||||||
|
{{ $app->name }}
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn-cancel" onclick="closeModal('edit-role-modal')">Cancel</button>
|
||||||
|
<button type="submit" class="action-btn" style="background-color:var(--accent-primary); color:white; border-color:transparent; font-weight:600; padding:8px 16px; border-radius:8px; cursor:pointer;">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assign User Roles Modal -->
|
||||||
|
<div id="assign-roles-modal" class="admin-modal">
|
||||||
|
<div class="admin-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Assign Roles to <span id="assign-user-name">User</span></h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('assign-roles-modal')">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="assign-roles-form" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Select Roles for User</label>
|
||||||
|
<div style="background:rgba(0,0,0,0.2); border:1px solid var(--card-border); border-radius:8px; padding:12px; max-height:200px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
|
||||||
|
@foreach($roles as $role)
|
||||||
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" name="roles[]" value="{{ $role->id }}" class="assign-user-role-checkbox" id="assign-user-role-{{ $role->id }}">
|
||||||
|
<div>
|
||||||
|
<strong>{{ $role->name }}</strong>
|
||||||
|
@if($role->description)
|
||||||
|
<div style="font-size:0.7rem; color:var(--text-muted);">{{ $role->description }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn-cancel" onclick="closeModal('assign-roles-modal')">Cancel</button>
|
||||||
|
<button type="submit" class="action-btn" style="background-color:var(--accent-primary); color:white; border-color:transparent; font-weight:600; padding:8px 16px; border-radius:8px; cursor:pointer;">Update Roles</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditRoleModal(id, name, description, appIds) {
|
||||||
|
const form = document.getElementById('edit-role-form');
|
||||||
|
form.action = `/admin/roles/${id}/update`;
|
||||||
|
|
||||||
|
document.getElementById('edit-role-name').value = name;
|
||||||
|
document.getElementById('edit-role-description').value = description || '';
|
||||||
|
|
||||||
|
// Reset and check assigned apps
|
||||||
|
document.querySelectorAll('.edit-role-app-checkbox').forEach(cb => {
|
||||||
|
cb.checked = appIds.includes(parseInt(cb.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
openModal('edit-role-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAssignRolesModal(userId, name, roleIds) {
|
||||||
|
const form = document.getElementById('assign-roles-form');
|
||||||
|
form.action = `/admin/users/${userId}/roles`;
|
||||||
|
|
||||||
|
document.getElementById('assign-user-name').innerText = name;
|
||||||
|
|
||||||
|
// Reset and check user's roles
|
||||||
|
document.querySelectorAll('.assign-user-role-checkbox').forEach(cb => {
|
||||||
|
cb.checked = roleIds.includes(parseInt(cb.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
openModal('assign-roles-modal');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<footer style="text-align: center; padding: 40px 0; font-size: 0.75rem; color: var(--text-muted); z-index: 10;">
|
<footer style="text-align: center; padding: 40px 0; font-size: 0.75rem; color: var(--text-muted); z-index: 10;">
|
||||||
© 2026 SingleLogin Systems. Authorized Access Only.
|
© 2026 SingleLogin Systems. Authorized Access Only.
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -363,19 +363,6 @@
|
|||||||
Login as Administrator
|
Login as Administrator
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="divider">or</div>
|
|
||||||
|
|
||||||
<!-- Microsoft Login Alternative for Admin -->
|
|
||||||
<a href="{{ route('auth.microsoft.redirect') }}" class="btn btn-microsoft" style="text-decoration: none;">
|
|
||||||
<div class="microsoft-icon-grid">
|
|
||||||
<div class="micro-rect r1"></div>
|
|
||||||
<div class="micro-rect r2"></div>
|
|
||||||
<div class="micro-rect r3"></div>
|
|
||||||
<div class="micro-rect r4"></div>
|
|
||||||
</div>
|
|
||||||
Sign in with Microsoft
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -551,7 +551,7 @@
|
|||||||
|
|
||||||
<div class="services-grid">
|
<div class="services-grid">
|
||||||
@foreach($services as $service)
|
@foreach($services as $service)
|
||||||
<div class="service-card" onclick="launchSSO('{{ $service['name'] }}', '{{ $service['url'] }}')" style="border-top: 4px solid {{ $service['color'] }}">
|
<div class="service-card" onclick="launchSSO('{{ $service['name'] }}', '{{ route('sso.launch', $service['id']) }}')" style="border-top: 4px solid {{ $service['color'] }}">
|
||||||
<div>
|
<div>
|
||||||
<div class="service-tag">{{ $service['tag'] }}</div>
|
<div class="service-tag">{{ $service['tag'] }}</div>
|
||||||
<div class="service-icon-wrapper" style="background-color: rgba({{ hexdec(substr($service['color'], 1, 2)) }}, {{ hexdec(substr($service['color'], 3, 2)) }}, {{ hexdec(substr($service['color'], 5, 2)) }}, 0.08)">
|
<div class="service-icon-wrapper" style="background-color: rgba({{ hexdec(substr($service['color'], 1, 2)) }}, {{ hexdec(substr($service['color'], 3, 2)) }}, {{ hexdec(substr($service['color'], 5, 2)) }}, 0.08)">
|
||||||
|
|||||||
@ -17,9 +17,11 @@
|
|||||||
Route::get('/auth/microsoft/redirect', [LoginController::class, 'redirectToMicrosoft'])->name('auth.microsoft.redirect');
|
Route::get('/auth/microsoft/redirect', [LoginController::class, 'redirectToMicrosoft'])->name('auth.microsoft.redirect');
|
||||||
Route::get('/auth/microsoft/callback', [LoginController::class, 'handleMicrosoftCallback'])->name('auth.microsoft.callback');
|
Route::get('/auth/microsoft/callback', [LoginController::class, 'handleMicrosoftCallback'])->name('auth.microsoft.callback');
|
||||||
|
|
||||||
// User Dashboard Portal (requires standard user role)
|
// User Dashboard Portal (requires standard user role or admin access)
|
||||||
Route::middleware(['auth', 'role:user'])->group(function () {
|
Route::middleware(['auth', 'role:user'])->group(function () {
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
Route::get('/sso/launch/{id}', [DashboardController::class, 'launchSSO'])->name('sso.launch');
|
||||||
|
Route::get('/sso/callback', [DashboardController::class, 'handleSSOCallback'])->name('sso.callback');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin Control Panel (requires admin role)
|
// Admin Control Panel (requires admin role)
|
||||||
@ -27,7 +29,15 @@
|
|||||||
Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard');
|
Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard');
|
||||||
Route::post('/admin/apps', [AdminController::class, 'storeApp'])->name('admin.apps.store');
|
Route::post('/admin/apps', [AdminController::class, 'storeApp'])->name('admin.apps.store');
|
||||||
Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy');
|
Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy');
|
||||||
Route::post('/admin/restrictions', [AdminController::class, 'storeRestriction'])->name('admin.restrictions.store');
|
|
||||||
Route::delete('/admin/restrictions/{id}', [AdminController::class, 'destroyRestriction'])->name('admin.restrictions.destroy');
|
|
||||||
Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');
|
Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');
|
||||||
|
|
||||||
|
// Roles CRUD
|
||||||
|
Route::post('/admin/roles', [AdminController::class, 'storeRole'])->name('admin.roles.store');
|
||||||
|
Route::post('/admin/roles/{id}/update', [AdminController::class, 'updateRole'])->name('admin.roles.update');
|
||||||
|
Route::delete('/admin/roles/{id}', [AdminController::class, 'destroyRole'])->name('admin.roles.destroy');
|
||||||
|
|
||||||
|
// User Roles & Overrides
|
||||||
|
Route::post('/admin/users/{id}/roles', [AdminController::class, 'updateUserRoles'])->name('admin.users.roles.update');
|
||||||
|
Route::post('/admin/overrides', [AdminController::class, 'storeUserOverride'])->name('admin.overrides.store');
|
||||||
|
Route::delete('/admin/overrides/{id}', [AdminController::class, 'destroyUserOverride'])->name('admin.overrides.destroy');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\App;
|
use App\Models\App;
|
||||||
use App\Models\AppRestriction;
|
use App\Models\Role;
|
||||||
|
use App\Models\UserAppOverride;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ public function test_admin_can_access_admin_dashboard_but_not_standard_users():
|
|||||||
->get('/admin/dashboard')
|
->get('/admin/dashboard')
|
||||||
->assertStatus(200);
|
->assertStatus(200);
|
||||||
|
|
||||||
// Standard user gets redirected to user dashboard
|
// Standard user gets allowed standard user access to dashboard (our middleware handles this)
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get('/admin/dashboard')
|
->get('/admin/dashboard')
|
||||||
->assertRedirect(route('dashboard'));
|
->assertRedirect(route('dashboard'));
|
||||||
@ -65,7 +66,7 @@ public function test_blocked_user_cannot_access_dashboard_and_gets_logged_out():
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test restricted services are hidden from specific users.
|
* Test restricted services are hidden from specific users via deny overrides.
|
||||||
*/
|
*/
|
||||||
public function test_restricted_service_is_not_displayed_to_the_restricted_user(): void
|
public function test_restricted_service_is_not_displayed_to_the_restricted_user(): void
|
||||||
{
|
{
|
||||||
@ -89,10 +90,18 @@ public function test_restricted_service_is_not_displayed_to_the_restricted_user(
|
|||||||
'tag' => 'HR Portal'
|
'tag' => 'HR Portal'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Restrict user from Keka HR
|
// Create a role and associate both apps
|
||||||
AppRestriction::create([
|
$role = Role::create(['name' => 'Employee']);
|
||||||
'email' => $user->email,
|
$role->apps()->sync([$app1->id, $app2->id]);
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
$user->roles()->sync([$role->id]);
|
||||||
|
|
||||||
|
// Restrict user from Keka HR using override
|
||||||
|
UserAppOverride::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
'app_id' => $app2->id,
|
'app_id' => $app2->id,
|
||||||
|
'type' => 'deny',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
@ -102,4 +111,81 @@ public function test_restricted_service_is_not_displayed_to_the_restricted_user(
|
|||||||
$response->assertSee('Outlook');
|
$response->assertSee('Outlook');
|
||||||
$response->assertDontSee('Keka HR');
|
$response->assertDontSee('Keka HR');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test additional services are displayed to specific users via allow overrides.
|
||||||
|
*/
|
||||||
|
public function test_additional_service_is_displayed_to_the_user(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Standard User',
|
||||||
|
'email' => 'user@sentientgeeks.com',
|
||||||
|
'role' => 'user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$app1 = App::create([
|
||||||
|
'name' => 'Outlook',
|
||||||
|
'url' => 'https://outlook.office.com',
|
||||||
|
'color' => '#0078d4',
|
||||||
|
'tag' => 'Microsoft 365'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$app2 = App::create([
|
||||||
|
'name' => 'Keka HR',
|
||||||
|
'url' => 'https://sentientgeeks.keka.com',
|
||||||
|
'color' => '#f27059',
|
||||||
|
'tag' => 'HR Portal'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a role that only has Outlook
|
||||||
|
$role = Role::create(['name' => 'Intern']);
|
||||||
|
$role->apps()->sync([$app1->id]);
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
$user->roles()->sync([$role->id]);
|
||||||
|
|
||||||
|
// Add Keka HR as extra service using override
|
||||||
|
UserAppOverride::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'app_id' => $app2->id,
|
||||||
|
'type' => 'allow',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get('/dashboard');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Outlook');
|
||||||
|
$response->assertSee('Keka HR');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test admin user cannot authenticate via Microsoft OAuth callback.
|
||||||
|
*/
|
||||||
|
public function test_admin_cannot_authenticate_via_microsoft_oauth_callback(): void
|
||||||
|
{
|
||||||
|
$admin = User::create([
|
||||||
|
'name' => 'Admin User',
|
||||||
|
'email' => 'admin@company.com',
|
||||||
|
'role' => 'admin',
|
||||||
|
'microsoft_id' => 'admin-microsoft-id-999'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
|
||||||
|
$mockUser = \Mockery::mock('Laravel\Socialite\Two\User');
|
||||||
|
|
||||||
|
$mockUser->shouldReceive('getId')->andReturn('admin-microsoft-id-999');
|
||||||
|
$mockUser->shouldReceive('getEmail')->andReturn('admin@company.com');
|
||||||
|
$mockUser->shouldReceive('getName')->andReturn('Admin User');
|
||||||
|
$mockUser->shouldReceive('getAvatar')->andReturn(null);
|
||||||
|
|
||||||
|
$mockSocialite->shouldReceive('user')->andReturn($mockUser);
|
||||||
|
\Laravel\Socialite\Facades\Socialite::shouldReceive('driver')->with('microsoft')->andReturn($mockSocialite);
|
||||||
|
|
||||||
|
$response = $this->get('/auth/microsoft/callback');
|
||||||
|
|
||||||
|
$response->assertRedirect(route('admin.login'));
|
||||||
|
$response->assertSessionHas('error', 'Security protocol: Administrators are restricted to email and password authentication only.');
|
||||||
|
$this->assertFalse(auth()->check());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user