diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000..4b05a27
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -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.
diff --git a/README.md b/README.md
index 5ad1377..72d39d1 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index 6ad566f..383293c 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -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.");
+ }
}
diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php
index 6d832af..5c90a6e 100644
--- a/app/Http/Controllers/LoginController.php
+++ b/app/Http/Controllers/LoginController.php
@@ -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);
diff --git a/app/Http/Middleware/EnsureRole.php b/app/Http/Middleware/EnsureRole.php
index 47892a7..52a8a88 100644
--- a/app/Http/Middleware/EnsureRole.php
+++ b/app/Http/Middleware/EnsureRole.php
@@ -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.');
diff --git a/app/Models/Setting.php b/app/Models/Setting.php
new file mode 100644
index 0000000..fc8dec9
--- /dev/null
+++ b/app/Models/Setting.php
@@ -0,0 +1,38 @@
+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]
+ );
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index b53d204..24d6b75 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -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%'])
diff --git a/database/migrations/2026_07_15_000000_create_settings_table.php b/database/migrations/2026_07_15_000000_create_settings_table.php
new file mode 100644
index 0000000..ce3b578
--- /dev/null
+++ b/database/migrations/2026_07_15_000000_create_settings_table.php
@@ -0,0 +1,29 @@
+id();
+ $table->string('key')->unique();
+ $table->text('value')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('settings');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index e214217..7a842ea 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -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']
+ );
}
}
diff --git a/public/Screenshot_10.png b/public/Screenshot_10.png
new file mode 100644
index 0000000..1667d6e
Binary files /dev/null and b/public/Screenshot_10.png differ
diff --git a/public/Screenshot_2.png b/public/Screenshot_2.png
index b1f497d..7df07e7 100644
Binary files a/public/Screenshot_2.png and b/public/Screenshot_2.png differ
diff --git a/public/Screenshot_9.png b/public/Screenshot_9.png
new file mode 100644
index 0000000..362380d
Binary files /dev/null and b/public/Screenshot_9.png differ
diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php
index 288aa2e..f1d5ac3 100644
--- a/resources/views/admin/dashboard.blade.php
+++ b/resources/views/admin/dashboard.blade.php
@@ -9,6 +9,9 @@
+
+
+