implement admin multiple role
This commit is contained in:
parent
9f0aeee412
commit
d2c286e326
74
DEPLOYMENT.md
Normal file
74
DEPLOYMENT.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Production Deployment & Data Persistence Guide
|
||||
|
||||
To ensure that user data (and other settings/services) is not lost when you deploy updates to the application, follow these guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 1. Do Not Use SQLite in Ephemeral Environments
|
||||
By default, the application is configured to use **SQLite** (`database/database.sqlite`).
|
||||
|
||||
SQLite is a file-based database. In modern hosting environments—such as **Heroku**, **AWS ECS**, **Docker containers**, or **digital ocean app platform**—the local container storage is ephemeral (destroyed and recreated on every deployment or restart).
|
||||
|
||||
### Recommended: Switch to a Managed Database (MySQL / PostgreSQL)
|
||||
For production deployments, change your database connection in your server's `.env` file to a persistent RDBMS like MySQL or PostgreSQL:
|
||||
|
||||
```ini
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=your-production-db-host.com
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=singlelogin
|
||||
DB_USERNAME=admin
|
||||
DB_PASSWORD=your_secure_password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. If You Must Use SQLite in Production
|
||||
If you deploy to a Virtual Private Server (VPS) like **DigitalOcean Droplet**, **Linode**, or **AWS EC2**, or if you use Docker with persistent volumes, you can continue using SQLite securely:
|
||||
|
||||
### Step A: Move the SQLite Database File to a Shared/Persistent Directory
|
||||
Do not leave the `database.sqlite` file inside the deployment root directory (which gets overwritten/cleaned on deployment). Instead, place it in a persistent folder (e.g. `/var/www/shared/` or `/data/`):
|
||||
|
||||
1. On your production server, create a directory for persistent data:
|
||||
```bash
|
||||
mkdir -p /var/www/shared
|
||||
```
|
||||
2. Move your existing database file there (so you don't lose current users):
|
||||
```bash
|
||||
mv /var/www/singlelogin/database/database.sqlite /var/www/shared/database.sqlite
|
||||
```
|
||||
3. Set the correct permissions so the web server can read and write to the database and its parent folder:
|
||||
```bash
|
||||
chown -R www-data:www-data /var/www/shared
|
||||
chmod -R 775 /var/www/shared
|
||||
```
|
||||
|
||||
### Step B: Configure the Absolute Path in `.env`
|
||||
Update the `DB_DATABASE` environment variable in your production `.env` file to point to this absolute persistent path:
|
||||
|
||||
```ini
|
||||
DB_CONNECTION=sqlite
|
||||
DB_DATABASE=/var/www/shared/database.sqlite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Safe Migration Commands on Deployment
|
||||
When deploying updates, make sure your deployment scripts run migrations safely:
|
||||
|
||||
* **DO RUN**:
|
||||
```bash
|
||||
php artisan migrate --force
|
||||
```
|
||||
This command runs only new, pending migration files and keeps all existing data safe. The `--force` flag allows it to run in production without prompting.
|
||||
|
||||
* **NEVER RUN**:
|
||||
```bash
|
||||
php artisan migrate:fresh --seed
|
||||
```
|
||||
This command drops all database tables, destroying all registered users and data, and then rebuilds the schema from scratch. Do not use this in production.
|
||||
|
||||
---
|
||||
|
||||
## 4. Git Configurations
|
||||
The SQLite database file `database.sqlite` is already ignored in git (`database/.gitignore`). Do not force-add or commit database files to your git repository, as doing so would cause your local database to overwrite your production database during deployment.
|
||||
@ -49,6 +49,10 @@ ## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Deployment & Persistence
|
||||
|
||||
To configure the application for production and ensure that no user data is lost during deployment, please refer to the [DEPLOYMENT.md](file:///D:/projects/singlelogin/DEPLOYMENT.md) guide.
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
@ -39,7 +39,9 @@ public function index()
|
||||
'roles_count' => Role::count(),
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact('admin', 'users', 'apps', 'roles', 'overrides', 'stats'));
|
||||
$defaultRoleName = \App\Models\Setting::get('default_role', 'Developer');
|
||||
|
||||
return view('admin.dashboard', compact('admin', 'users', 'apps', 'roles', 'overrides', 'stats', 'defaultRoleName'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -269,4 +271,56 @@ public function toggleBlockUser(Request $request, $id)
|
||||
$status = $user->is_blocked ? 'blocked' : 'unblocked';
|
||||
return redirect()->route('admin.dashboard')->with('success', "User account has been {$status} successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the default role for new users.
|
||||
*/
|
||||
public function updateDefaultRole(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'default_role' => 'required|string|exists:roles,name',
|
||||
]);
|
||||
|
||||
\App\Models\Setting::set('default_role', $request->default_role);
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Default role for new users updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the admin privilege of a user.
|
||||
*/
|
||||
public function toggleAdminPrivilege(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
// Security: Prevent modifying own admin privileges
|
||||
if ($user->id === Auth::id()) {
|
||||
return redirect()->route('admin.dashboard')->with('error', 'Security violation: You cannot modify your own administrator privilege.');
|
||||
}
|
||||
|
||||
if ($user->role === 'admin') {
|
||||
$user->role = 'user';
|
||||
|
||||
// Detach Admin role from pivot if it exists
|
||||
$adminRole = Role::where('name', 'Admin')->first();
|
||||
if ($adminRole) {
|
||||
$user->roles()->detach($adminRole->id);
|
||||
}
|
||||
$user->save();
|
||||
$status = 'revoked';
|
||||
} else {
|
||||
$user->role = 'admin';
|
||||
|
||||
// Attach Admin role to pivot (create if missing)
|
||||
$adminRole = Role::firstOrCreate(
|
||||
['name' => 'Admin'],
|
||||
['description' => 'Administrator access role.']
|
||||
);
|
||||
$user->roles()->syncWithoutDetaching([$adminRole->id]);
|
||||
$user->save();
|
||||
$status = 'granted';
|
||||
}
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', "Admin privilege has been {$status} for {$user->name} successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,16 @@ public function handleMicrosoftCallback(Request $request)
|
||||
'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token,
|
||||
'avatar' => $microsoftUser->getAvatar() ?? $user->avatar,
|
||||
]);
|
||||
|
||||
// Assign default role if they have no roles assigned
|
||||
if ($user->roles()->count() === 0) {
|
||||
$defaultRoleName = \App\Models\Setting::get('default_role', 'Developer');
|
||||
$defaultRole = \App\Models\Role::firstOrCreate(
|
||||
['name' => $defaultRoleName],
|
||||
['description' => 'Default application role.']
|
||||
);
|
||||
$user->roles()->sync([$defaultRole->id]);
|
||||
}
|
||||
} else {
|
||||
// Block registration if domain is not allowed
|
||||
if (!$isSentientGeeks) {
|
||||
@ -140,6 +150,14 @@ public function handleMicrosoftCallback(Request $request)
|
||||
'avatar' => $microsoftUser->getAvatar(),
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
// Assign configured default role
|
||||
$defaultRoleName = \App\Models\Setting::get('default_role', 'Developer');
|
||||
$defaultRole = \App\Models\Role::firstOrCreate(
|
||||
['name' => $defaultRoleName],
|
||||
['description' => 'Default application role.']
|
||||
);
|
||||
$user->roles()->sync([$defaultRole->id]);
|
||||
}
|
||||
|
||||
Auth::login($user, true);
|
||||
|
||||
@ -26,11 +26,16 @@ public function handle(Request $request, Closure $next, string $role): Response
|
||||
return redirect()->route('login')->with('error', 'Your account has been blocked. Please contact the administrator.');
|
||||
}
|
||||
|
||||
if (auth()->user()->role !== $role) {
|
||||
if (auth()->user()->role === 'admin') {
|
||||
if ($role === 'user') {
|
||||
return $next($request);
|
||||
}
|
||||
$user = auth()->user();
|
||||
$hasRole = ($user->role === $role) || $user->hasRole($role);
|
||||
|
||||
if (!$hasRole) {
|
||||
$isAdmin = ($user->role === 'admin') || $user->hasRole('admin');
|
||||
if ($role === 'user' && $isAdmin) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($isAdmin) {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
return redirect()->route('dashboard')->with('error', 'Unauthorized access.');
|
||||
|
||||
38
app/Models/Setting.php
Normal file
38
app/Models/Setting.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
|
||||
#[Fillable(['key', 'value'])]
|
||||
class Setting extends Model
|
||||
{
|
||||
/**
|
||||
* Get a setting value by key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
$setting = self::where('key', $key)->first();
|
||||
return $setting ? $setting->value : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return \App\Models\Setting
|
||||
*/
|
||||
public static function set(string $key, $value)
|
||||
{
|
||||
return self::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,22 @@ class User extends Authenticatable
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
return $this->role === 'admin' || $this->hasRole('admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a specific role by name.
|
||||
*/
|
||||
public function hasRole(string $roleName): bool
|
||||
{
|
||||
if ($this->roles->isEmpty()) {
|
||||
$defaultRoleName = \App\Models\Setting::get('default_role', 'Developer');
|
||||
return strtolower($roleName) === strtolower($defaultRoleName);
|
||||
}
|
||||
|
||||
return $this->roles->contains(function ($r) use ($roleName) {
|
||||
return strtolower($r->name) === strtolower($roleName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,14 +67,23 @@ public function authorizedApps()
|
||||
|
||||
$roleIds = $this->roles()->pluck('roles.id')->toArray();
|
||||
|
||||
// If no roles are assigned, fallback to configured default role first
|
||||
if (empty($roleIds)) {
|
||||
$defaultRoleName = \App\Models\Setting::get('default_role', 'Developer');
|
||||
$defaultRole = \App\Models\Role::where('name', $defaultRoleName)->first();
|
||||
if ($defaultRole) {
|
||||
$roleIds = [$defaultRole->id];
|
||||
}
|
||||
}
|
||||
|
||||
// Get apps associated with user's roles
|
||||
$roleAppIds = \DB::table('app_role')
|
||||
->whereIn('role_id', $roleIds)
|
||||
->pluck('app_id')
|
||||
->toArray();
|
||||
|
||||
// If no roles are assigned, user gets default access to Outlook email and Drive apps if registered
|
||||
if (empty($roleIds)) {
|
||||
// If still no roles/apps resolved, user gets default access to Outlook email and Drive apps if registered
|
||||
if (empty($roleAppIds)) {
|
||||
$defaultAppIds = App::where(function ($query) {
|
||||
$query->whereRaw('LOWER(name) LIKE ?', ['%outlook%'])
|
||||
->orWhereRaw('LOWER(name) LIKE ?', ['%mail%'])
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
<?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::create('settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('settings');
|
||||
}
|
||||
};
|
||||
@ -149,5 +149,11 @@ public function run(): void
|
||||
['user_id' => $user1->id, 'app_id' => $keka->id],
|
||||
['type' => 'allow']
|
||||
);
|
||||
|
||||
// Seed default role setting
|
||||
\App\Models\Setting::updateOrCreate(
|
||||
['key' => 'default_role'],
|
||||
['value' => 'Developer']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/Screenshot_10.png
Normal file
BIN
public/Screenshot_10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 473 KiB After Width: | Height: | Size: 252 KiB |
BIN
public/Screenshot_9.png
Normal file
BIN
public/Screenshot_9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
@ -9,6 +9,9 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #0b0f19;
|
||||
@ -681,11 +684,28 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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);">
|
||||
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 600; margin-bottom: 16px; color: #a5b4fc;">Manage Roles & Services</h3>
|
||||
|
||||
<!-- Default Role Configuration Form -->
|
||||
<form action="{{ route('admin.settings.default-role') }}" method="POST" style="margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
@csrf
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Default Role for New Users</label>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<select name="default_role" required class="form-input" style="flex: 1; padding:10px; background:rgba(255,255,255,0.03); border:1px solid var(--card-border); border-radius:8px; color:white; outline: none; font-size: 0.85rem;">
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}" {{ $defaultRoleName === $role->name ? 'selected' : '' }} style="background-color: #1e1e38; color: white;">
|
||||
{{ $role->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; font-size:0.85rem; border-radius:8px; padding:10px 16px; border:none; cursor:pointer; font-weight:600; white-space: nowrap;">Save Default</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Add Role Form -->
|
||||
<form action="{{ route('admin.roles.store') }}" method="POST" style="margin-bottom: 30px;">
|
||||
@csrf
|
||||
@ -915,6 +935,10 @@
|
||||
<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>
|
||||
@elseif($usr->role !== 'admin')
|
||||
<div style="display:flex; flex-wrap:wrap; gap:4px; margin-top:6px;">
|
||||
<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;">{{ $defaultRoleName }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@ -938,21 +962,32 @@
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right; white-space: nowrap;">
|
||||
@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;">
|
||||
@csrf
|
||||
<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;
|
||||
{{ $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' : 'Block' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span style="font-size:0.75rem; color:var(--text-muted); font-style:italic;">You (Admin)</span>
|
||||
@endif
|
||||
@if($usr->id !== $admin->id)
|
||||
<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>
|
||||
|
||||
@if($usr->isAdmin())
|
||||
<button class="action-btn" onclick="confirmAdminToggle({{ $usr->id }}, '{{ addslashes($usr->name) }}', 'revoke')"
|
||||
style="color:#ef4444; border-color:rgba(239,68,68,0.3); background-color:rgba(239,68,68,0.05); font-size:0.75rem; padding:6px 12px; margin-right:4px; font-weight:600;">
|
||||
Revoke Admin
|
||||
</button>
|
||||
@else
|
||||
<button class="action-btn" onclick="confirmAdminToggle({{ $usr->id }}, '{{ addslashes($usr->name) }}', 'grant')"
|
||||
style="color:#f59e0b; border-color:rgba(245,158,11,0.3); background-color:rgba(245,158,11,0.05); font-size:0.75rem; padding:6px 12px; margin-right:4px; font-weight:600; box-shadow: 0 0 10px rgba(245,158,11,0.15);">
|
||||
Give Admin Privilege
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.users.toggle-block', $usr->id) }}" method="POST" style="display:inline;">
|
||||
@csrf
|
||||
<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;
|
||||
{{ $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' : 'Block' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span style="font-size:0.75rem; color:var(--text-muted); font-style:italic;">You (Admin)</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
@ -1068,6 +1103,9 @@
|
||||
<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)
|
||||
@if(strtolower($role->name) === 'admin')
|
||||
@continue
|
||||
@endif
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" name="roles[]" value="{{ $role->id }}" class="assign-user-role-checkbox" id="assign-user-role-{{ $role->id }}">
|
||||
<div>
|
||||
@ -1148,6 +1186,43 @@ function openEditAppModal(id, name, url, tag, iconSvg, color, desc, logoPath) {
|
||||
|
||||
openModal('edit-app-modal');
|
||||
}
|
||||
|
||||
function confirmAdminToggle(userId, userName, actionType) {
|
||||
const isGrant = actionType === 'grant';
|
||||
const title = isGrant ? 'Grant Admin Privilege?' : 'Revoke Admin Privilege?';
|
||||
const text = isGrant
|
||||
? `Are you sure you want to grant admin privilege to ${userName}? They will be able to access the admin control panel.`
|
||||
: `Are you sure you want to revoke admin privilege from ${userName}? They will no longer have administrative access.`;
|
||||
const confirmButtonText = isGrant ? 'Yes, grant it!' : 'Yes, revoke it!';
|
||||
const confirmButtonColor = isGrant ? '#f59e0b' : '#ef4444';
|
||||
|
||||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: confirmButtonColor,
|
||||
cancelButtonColor: '#374151',
|
||||
confirmButtonText: confirmButtonText,
|
||||
background: '#1f2937',
|
||||
color: '#f3f4f6'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = "{{ route('admin.users.toggle-admin', ':id') }}".replace(':id', userId);
|
||||
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = '_token';
|
||||
csrfInput.value = '{{ csrf_token() }}';
|
||||
form.appendChild(csrfInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<footer style="text-align: center; padding: 40px 0; font-size: 0.75rem; color: var(--text-muted); z-index: 10;">
|
||||
|
||||
@ -598,12 +598,12 @@
|
||||
<span class="nav-profile-email">{{ $user->email }}</span>
|
||||
<span class="meta-dot">•</span>
|
||||
<span class="nav-badge">
|
||||
@if($user->isAdmin())
|
||||
Admin
|
||||
@elseif($user->roles->isNotEmpty())
|
||||
@if($user->roles->isNotEmpty())
|
||||
{{ $user->roles->pluck('name')->implode(', ') }}
|
||||
@elseif($user->role === 'admin')
|
||||
Admin
|
||||
@else
|
||||
{{ ucfirst($user->role) }}
|
||||
{{ \App\Models\Setting::get('default_role', 'Developer') }}
|
||||
@endif
|
||||
</span>
|
||||
@if($user->microsoft_id)
|
||||
@ -614,11 +614,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Right: Actions -->
|
||||
<div class="nav-actions">
|
||||
@if($user->role === 'admin')
|
||||
<a href="{{ route('admin.dashboard') }}" class="nav-action-btn admin" title="Admin Panel">
|
||||
Admin
|
||||
Go to Admin Dashboard
|
||||
</a>
|
||||
@elseif($user->hasRole('admin'))
|
||||
<a href="{{ route('admin.dashboard') }}" target="_blank" class="nav-action-btn admin" title="Admin Panel">
|
||||
Go to Admin Dashboard
|
||||
</a>
|
||||
@endif
|
||||
<form action="{{ route('logout') }}" method="POST" id="logout-form" style="display: none;">
|
||||
|
||||
@ -37,11 +37,13 @@
|
||||
Route::post('/admin/apps/{id}/update', [AdminController::class, 'updateApp'])->name('admin.apps.update');
|
||||
Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy');
|
||||
Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');
|
||||
Route::post('/admin/users/{id}/toggle-admin', [AdminController::class, 'toggleAdminPrivilege'])->name('admin.users.toggle-admin');
|
||||
|
||||
// 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');
|
||||
Route::post('/admin/settings/default-role', [AdminController::class, 'updateDefaultRole'])->name('admin.settings.default-role');
|
||||
|
||||
// User Roles & Overrides
|
||||
Route::post('/admin/users/{id}/roles', [AdminController::class, 'updateUserRoles'])->name('admin.users.roles.update');
|
||||
|
||||
@ -273,4 +273,165 @@ public function test_sso_launch_redirects_to_microsoft_for_all_services_if_user_
|
||||
|
||||
$response->assertRedirect('https://login.microsoftonline.com/common/oauth2/v2.0/authorize');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user with multiple roles including Admin logs in via general route,
|
||||
* sees user dashboard, displays all roles, and has Go to Admin Dashboard target_blank link.
|
||||
*/
|
||||
public function test_user_multiple_roles_admin_link(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => 'Multi Role User',
|
||||
'email' => 'multi@sentientgeeks.com',
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
$pmRole = Role::create(['name' => 'Project Manager']);
|
||||
$adminRole = Role::create(['name' => 'Admin']);
|
||||
|
||||
$user->roles()->sync([$pmRole->id, $adminRole->id]);
|
||||
|
||||
// User dashboard can be accessed
|
||||
$response = $this->actingAs($user)
|
||||
->get('/dashboard');
|
||||
|
||||
$response->assertStatus(200);
|
||||
// At the top they will see other role i.e. admin
|
||||
$response->assertSee('Project Manager, Admin');
|
||||
// And they will see a link Go to Admin Dashboard which has target="_blank"
|
||||
$response->assertSee('href="' . route('admin.dashboard') . '" target="_blank" class="nav-action-btn admin"', false);
|
||||
$response->assertSee('Go to Admin Dashboard');
|
||||
|
||||
// And they can access the Admin Dashboard
|
||||
$responseAdmin = $this->actingAs($user)
|
||||
->get('/admin/dashboard');
|
||||
$responseAdmin->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that new user registration / login defaults to the configured role.
|
||||
*/
|
||||
public function test_new_user_registration_default_role(): void
|
||||
{
|
||||
// 1. Initially configuration fallback is Developer
|
||||
$devRole = Role::create(['name' => 'Developer']);
|
||||
$otherRole = Role::create(['name' => 'Team Lead']);
|
||||
|
||||
// Mock Socialite callback for a brand new user
|
||||
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
|
||||
$mockUser = \Mockery::mock('Laravel\Socialite\Two\User');
|
||||
|
||||
$mockUser->shouldReceive('getId')->andReturn('new-microsoft-id-888');
|
||||
$mockUser->shouldReceive('getEmail')->andReturn('newuser@sentientgeeks.com');
|
||||
$mockUser->shouldReceive('getName')->andReturn('New User');
|
||||
$mockUser->shouldReceive('getAvatar')->andReturn(null);
|
||||
|
||||
$mockSocialite->shouldReceive('user')->andReturn($mockUser);
|
||||
\Laravel\Socialite\Facades\Socialite::shouldReceive('driver')->with('microsoft')->andReturn($mockSocialite);
|
||||
|
||||
// Login as new user
|
||||
$response = $this->get('/auth/microsoft/callback');
|
||||
$response->assertRedirect(route('dashboard'));
|
||||
|
||||
$newUser = User::where('email', 'newuser@sentientgeeks.com')->first();
|
||||
$this->assertNotNull($newUser);
|
||||
$this->assertTrue($newUser->hasRole('Developer'));
|
||||
$this->assertFalse($newUser->hasRole('Team Lead'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that new user registration / login defaults to the custom configured role.
|
||||
*/
|
||||
public function test_new_user_registration_custom_default_role(): void
|
||||
{
|
||||
$devRole = Role::create(['name' => 'Developer']);
|
||||
$otherRole = Role::create(['name' => 'Team Lead']);
|
||||
|
||||
// Admin updates the configuration
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@company.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post(route('admin.settings.default-role'), [
|
||||
'default_role' => 'Team Lead',
|
||||
])
|
||||
->assertRedirect(route('admin.dashboard'));
|
||||
|
||||
$this->assertEquals('Team Lead', \App\Models\Setting::get('default_role'));
|
||||
|
||||
// Mock Socialite callback for second new user
|
||||
$mockSocialite2 = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
|
||||
$mockUser2 = \Mockery::mock('Laravel\Socialite\Two\User');
|
||||
|
||||
$mockUser2->shouldReceive('getId')->andReturn('new-microsoft-id-999');
|
||||
$mockUser2->shouldReceive('getEmail')->andReturn('anotheruser@sentientgeeks.com');
|
||||
$mockUser2->shouldReceive('getName')->andReturn('Another User');
|
||||
$mockUser2->shouldReceive('getAvatar')->andReturn(null);
|
||||
|
||||
$mockSocialite2->shouldReceive('user')->andReturn($mockUser2);
|
||||
\Laravel\Socialite\Facades\Socialite::shouldReceive('driver')->with('microsoft')->andReturn($mockSocialite2);
|
||||
|
||||
// Login as second new user
|
||||
$response2 = $this->get('/auth/microsoft/callback');
|
||||
$response2->assertRedirect(route('dashboard'));
|
||||
|
||||
$anotherUser = User::where('email', 'anotheruser@sentientgeeks.com')->first();
|
||||
$this->assertNotNull($anotherUser);
|
||||
$this->assertTrue($anotherUser->hasRole('Team Lead'));
|
||||
$this->assertFalse($anotherUser->hasRole('Developer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test granting and revoking admin privileges to/from other users.
|
||||
*/
|
||||
public function test_admin_toggle_privileges(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@company.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$targetUser = User::create([
|
||||
'name' => 'Regular Dev',
|
||||
'email' => 'dev@sentientgeeks.com',
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class);
|
||||
|
||||
// 1. Grant admin privilege
|
||||
$response = $this->actingAs($admin)
|
||||
->post(route('admin.users.toggle-admin', $targetUser->id));
|
||||
|
||||
$response->assertRedirect(route('admin.dashboard'));
|
||||
|
||||
$targetUser = $targetUser->fresh();
|
||||
$this->assertTrue($targetUser->isAdmin());
|
||||
$this->assertEquals('admin', $targetUser->role);
|
||||
$this->assertTrue($targetUser->hasRole('Admin'));
|
||||
|
||||
// 2. Revoke admin privilege
|
||||
$response2 = $this->actingAs($admin)
|
||||
->post(route('admin.users.toggle-admin', $targetUser->id));
|
||||
|
||||
$response2->assertRedirect(route('admin.dashboard'));
|
||||
|
||||
$targetUser = $targetUser->fresh();
|
||||
$this->assertFalse($targetUser->isAdmin());
|
||||
$this->assertEquals('user', $targetUser->role);
|
||||
$this->assertFalse($targetUser->hasRole('Admin'));
|
||||
|
||||
// 3. Security: Prevent toggling self
|
||||
$response3 = $this->actingAs($admin)
|
||||
->post(route('admin.users.toggle-admin', $admin->id));
|
||||
|
||||
$response3->assertRedirect(route('admin.dashboard'));
|
||||
$this->assertTrue($admin->fresh()->isAdmin());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user