diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index 4fe1cb9..69277b4 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -5,6 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use App\Models\PersonalApp;
use Laravel\Socialite\Facades\Socialite;
class DashboardController extends Controller
@@ -19,7 +20,10 @@ public function index()
// Get authorized apps for this user
$services = $user->authorizedApps();
- return view('dashboard', compact('user', 'services'));
+ // Get personal custom apps for this user
+ $personalApps = $user->personalApps()->orderBy('name', 'asc')->get();
+
+ return view('dashboard', compact('user', 'services', 'personalApps'));
}
/**
@@ -148,4 +152,94 @@ public function launchApp(Request $request)
return view('sso.launch-app', compact('name', 'url', 'protocol'));
}
+
+ /**
+ * Store a new user custom personal app.
+ */
+ public function storePersonalApp(Request $request)
+ {
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ 'url' => 'required|url|max:255',
+ 'logo' => 'nullable|file|mimes:jpg,jpeg,png,webp,svg|max:2048',
+ 'color' => 'nullable|string|max:20',
+ 'desc' => 'nullable|string',
+ 'tag' => 'nullable|string|max:255',
+ ]);
+
+ $logoPath = null;
+ if ($request->hasFile('logo')) {
+ $file = $request->file('logo');
+ $filename = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
+ $file->move(public_path('uploads/logos'), $filename);
+ $logoPath = 'uploads/logos/' . $filename;
+ }
+
+ Auth::user()->personalApps()->create([
+ 'name' => $request->name,
+ 'url' => $request->url,
+ 'logo' => $logoPath,
+ 'color' => $request->color ?? '#4f46e5',
+ 'desc' => $request->desc,
+ 'tag' => $request->tag ?? 'Personal',
+ ]);
+
+ return redirect()->route('dashboard')->with('success', 'Custom app added successfully.');
+ }
+
+ /**
+ * Update an existing user custom personal app.
+ */
+ public function updatePersonalApp(Request $request, $id)
+ {
+ $app = Auth::user()->personalApps()->findOrFail($id);
+
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ 'url' => 'required|url|max:255',
+ 'logo' => 'nullable|file|mimes:jpg,jpeg,png,webp,svg|max:2048',
+ 'color' => 'nullable|string|max:20',
+ 'desc' => 'nullable|string',
+ 'tag' => 'nullable|string|max:255',
+ ]);
+
+ $logoPath = $app->logo;
+ if ($request->hasFile('logo')) {
+ // Delete old file
+ if ($app->logo && file_exists(public_path($app->logo))) {
+ @unlink(public_path($app->logo));
+ }
+ $file = $request->file('logo');
+ $filename = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
+ $file->move(public_path('uploads/logos'), $filename);
+ $logoPath = 'uploads/logos/' . $filename;
+ }
+
+ $app->update([
+ 'name' => $request->name,
+ 'url' => $request->url,
+ 'logo' => $logoPath,
+ 'color' => $request->color ?? '#4f46e5',
+ 'desc' => $request->desc,
+ 'tag' => $request->tag ?? 'Personal',
+ ]);
+
+ return redirect()->route('dashboard')->with('success', 'Custom app updated successfully.');
+ }
+
+ /**
+ * Delete a user custom personal app.
+ */
+ public function destroyPersonalApp($id)
+ {
+ $app = Auth::user()->personalApps()->findOrFail($id);
+
+ if ($app->logo && file_exists(public_path($app->logo))) {
+ @unlink(public_path($app->logo));
+ }
+
+ $app->delete();
+
+ return redirect()->route('dashboard')->with('success', 'Custom app deleted successfully.');
+ }
}
diff --git a/app/Models/PersonalApp.php b/app/Models/PersonalApp.php
new file mode 100644
index 0000000..badb87a
--- /dev/null
+++ b/app/Models/PersonalApp.php
@@ -0,0 +1,18 @@
+belongsTo(User::class);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index 24d6b75..e07425f 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -56,6 +56,14 @@ public function overrides()
return $this->hasMany(UserAppOverride::class);
}
+ /**
+ * Get the personal custom apps for this user.
+ */
+ public function personalApps()
+ {
+ return $this->hasMany(PersonalApp::class);
+ }
+
/**
* Get the authorized apps for this user, computed from roles and overrides.
*/
diff --git a/database/migrations/2026_07_16_114330_create_personal_apps_table.php b/database/migrations/2026_07_16_114330_create_personal_apps_table.php
new file mode 100644
index 0000000..6936bd0
--- /dev/null
+++ b/database/migrations/2026_07_16_114330_create_personal_apps_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
+ $table->string('name');
+ $table->string('url');
+ $table->string('logo')->nullable();
+ $table->string('color')->default('#4f46e5');
+ $table->text('desc')->nullable();
+ $table->string('tag')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('personal_apps');
+ }
+};
diff --git a/public/uploads/logos/1784202607_6a58c56fef7f9.png b/public/uploads/logos/1784202607_6a58c56fef7f9.png
new file mode 100644
index 0000000..58afdb4
Binary files /dev/null and b/public/uploads/logos/1784202607_6a58c56fef7f9.png differ
diff --git a/public/uploads/logos/1784202977_6a58c6e1eb311.png b/public/uploads/logos/1784202977_6a58c6e1eb311.png
new file mode 100644
index 0000000..58afdb4
Binary files /dev/null and b/public/uploads/logos/1784202977_6a58c6e1eb311.png differ
diff --git a/public/uploads/logos/1784203121_6a58c77104b2d.png b/public/uploads/logos/1784203121_6a58c77104b2d.png
new file mode 100644
index 0000000..58afdb4
Binary files /dev/null and b/public/uploads/logos/1784203121_6a58c77104b2d.png differ
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
index da0407b..8f1556f 100644
--- a/resources/views/dashboard.blade.php
+++ b/resources/views/dashboard.blade.php
@@ -22,6 +22,138 @@
--info-color: #06b6d4;
}
+ .personal-app-card:hover .personal-app-actions {
+ opacity: 1 !important;
+ pointer-events: auto !important;
+ }
+
+ /* Personal Modal Styling */
+ .personal-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(11, 15, 25, 0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ pointer-events: none;
+ transition: all 0.3s ease;
+ }
+
+ .personal-modal.active {
+ opacity: 1;
+ pointer-events: all;
+ }
+
+ .personal-modal-content {
+ background-color: rgba(25, 30, 45, 0.9);
+ 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.275);
+ max-height: 90vh;
+ overflow-y: auto;
+ }
+
+ .personal-modal.active .personal-modal-content {
+ transform: scale(1);
+ }
+
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+ padding-bottom: 12px;
+ }
+
+ .modal-title {
+ font-family: 'Outfit', sans-serif;
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: #a5b4fc;
+ }
+
+ .modal-close {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 1.5rem;
+ cursor: pointer;
+ transition: color 0.2s;
+ }
+
+ .modal-close:hover {
+ color: white;
+ }
+
+ .form-group {
+ margin-bottom: 16px;
+ }
+
+ .form-label {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ }
+
+ .form-input {
+ width: 100%;
+ padding: 10px 12px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid var(--card-border);
+ border-radius: 8px;
+ color: white;
+ outline: none;
+ font-size: 0.85rem;
+ transition: all 0.2s;
+ }
+
+ .form-input:focus {
+ border-color: var(--accent-primary);
+ background: rgba(255, 255, 255, 0.06);
+ }
+
+ .modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 24px;
+ border-top: 1px solid rgba(255,255,255,0.08);
+ padding-top: 16px;
+ }
+
+ .btn-cancel {
+ padding: 8px 16px;
+ border-radius: 8px;
+ border: 1px solid var(--card-border);
+ background: transparent;
+ color: var(--text-main);
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+ }
+
+ .btn-cancel:hover {
+ background: rgba(255,255,255,0.05);
+ }
+
* {
box-sizing: border-box;
margin: 0;
@@ -690,6 +822,71 @@
+
+ My Custom Apps
+
+
+ @else
+
{{ strtoupper($pApp->name) }}
+ @if($pApp->tag)
+