subhajit_changes_06_07 #9

Merged
subhajit.pal merged 3 commits from subhajit_changes_06_07 into main 2026-07-16 12:12:51 +00:00
16 changed files with 525 additions and 30 deletions
Showing only changes of commit d2c286e326 - Show all commits

74
DEPLOYMENT.md Normal file
View 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.

View File

@ -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.

View File

@ -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.");
}
}

View File

@ -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);

View File

@ -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
View 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]
);
}
}

View File

@ -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%'])

View File

@ -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');
}
};

View File

@ -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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -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;">

View File

@ -598,12 +598,12 @@
<span class="nav-profile-email">{{ $user->email }}</span>
<span class="meta-dot">&bull;</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;">

View File

@ -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');

View File

@ -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());
}
}