From 462762a13f720cb60272f8ad0a26b93b7ac9d9b6 Mon Sep 17 00:00:00 2001 From: subhajit Date: Thu, 16 Jul 2026 17:31:32 +0530 Subject: [PATCH] implement new feature --- app/Http/Controllers/DashboardController.php | 96 +++++- app/Models/PersonalApp.php | 18 + app/Models/User.php | 8 + ...7_16_114330_create_personal_apps_table.php | 34 ++ .../logos/1784202607_6a58c56fef7f9.png | Bin 0 -> 5359 bytes .../logos/1784202977_6a58c6e1eb311.png | Bin 0 -> 5359 bytes .../logos/1784203121_6a58c77104b2d.png | Bin 0 -> 5359 bytes resources/views/dashboard.blade.php | 318 ++++++++++++++++++ routes/web.php | 5 + tests/Feature/SecurityAndAdminTest.php | 83 +++++ 10 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 app/Models/PersonalApp.php create mode 100644 database/migrations/2026_07_16_114330_create_personal_apps_table.php create mode 100644 public/uploads/logos/1784202607_6a58c56fef7f9.png create mode 100644 public/uploads/logos/1784202977_6a58c6e1eb311.png create mode 100644 public/uploads/logos/1784203121_6a58c77104b2d.png 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 0000000000000000000000000000000000000000..58afdb47c254d4a7c2dde4e8aae1d6de6c3a1968 GIT binary patch literal 5359 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 N002ovPDHLkV1j;gQP2PY literal 0 HcmV?d00001 diff --git a/public/uploads/logos/1784202977_6a58c6e1eb311.png b/public/uploads/logos/1784202977_6a58c6e1eb311.png new file mode 100644 index 0000000000000000000000000000000000000000..58afdb47c254d4a7c2dde4e8aae1d6de6c3a1968 GIT binary patch literal 5359 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 N002ovPDHLkV1j;gQP2PY literal 0 HcmV?d00001 diff --git a/public/uploads/logos/1784203121_6a58c77104b2d.png b/public/uploads/logos/1784203121_6a58c77104b2d.png new file mode 100644 index 0000000000000000000000000000000000000000..58afdb47c254d4a7c2dde4e8aae1d6de6c3a1968 GIT binary patch literal 5359 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 N002ovPDHLkV1j;gQP2PY literal 0 HcmV?d00001 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

+ +
+ +
+ @forelse($personalApps as $pApp) +
+ + +
+ +
+ +
+ @csrf + @method('DELETE') + +
+
+ + +
+ @if($pApp->logo) + {{ $pApp->name }} + @else +
+ {{ strtoupper(substr($pApp->name, 0, 1)) }} +
+ @endif +
+
+ + +

{{ strtoupper($pApp->name) }}

+ @if($pApp->tag) +
{{ $pApp->tag }}
+ @endif +
+ @empty +
+ No custom apps added yet. Click "+ Add Custom App" to add your own shortcuts! +
+ @endforelse +
+
+ @@ -707,6 +904,96 @@ + +
+
+ +
+ @csrf +
+ + +
+
+ + +
+
+ + +
Supported: PNG, JPG, WEBP, SVG (Max 2MB)
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+ +
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+
+ diff --git a/routes/web.php b/routes/web.php index 8cf42a1..b2e9c16 100644 --- a/routes/web.php +++ b/routes/web.php @@ -27,6 +27,11 @@ Route::get('/sso/launch/{id}', [DashboardController::class, 'launchSSO'])->name('sso.launch'); Route::get('/sso/callback', [DashboardController::class, 'handleSSOCallback'])->name('sso.callback'); Route::get('/sso/launch-app', [DashboardController::class, 'launchApp'])->name('sso.launch-app'); + + // Personal custom apps management + Route::post('/dashboard/personal-apps', [DashboardController::class, 'storePersonalApp'])->name('dashboard.personal-apps.store'); + Route::post('/dashboard/personal-apps/{id}/update', [DashboardController::class, 'updatePersonalApp'])->name('dashboard.personal-apps.update'); + Route::delete('/dashboard/personal-apps/{id}', [DashboardController::class, 'destroyPersonalApp'])->name('dashboard.personal-apps.destroy'); }); diff --git a/tests/Feature/SecurityAndAdminTest.php b/tests/Feature/SecurityAndAdminTest.php index 3cba3ce..47883bf 100644 --- a/tests/Feature/SecurityAndAdminTest.php +++ b/tests/Feature/SecurityAndAdminTest.php @@ -434,4 +434,87 @@ public function test_admin_toggle_privileges(): void $response3->assertRedirect(route('admin.dashboard')); $this->assertTrue($admin->fresh()->isAdmin()); } + + /** + * Test user personal custom apps CRUD and isolation. + */ + public function test_user_personal_custom_apps(): void + { + $user1 = User::create([ + 'name' => 'User One', + 'email' => 'user1@sentientgeeks.com', + 'role' => 'user', + ]); + + $user2 = User::create([ + 'name' => 'User Two', + 'email' => 'user2@sentientgeeks.com', + 'role' => 'user', + ]); + + $this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class); + + // 1. Create personal app + $response = $this->actingAs($user1) + ->post(route('dashboard.personal-apps.store'), [ + 'name' => 'My Test App', + 'url' => 'https://example.com/test', + 'color' => '#123456', + 'tag' => 'Testing', + 'desc' => 'Some description' + ]); + + $response->assertRedirect(route('dashboard')); + $this->assertDatabaseHas('personal_apps', [ + 'user_id' => $user1->id, + 'name' => 'My Test App', + 'url' => 'https://example.com/test', + 'color' => '#123456' + ]); + + $personalApp = \App\Models\PersonalApp::where('name', 'My Test App')->first(); + $this->assertNotNull($personalApp); + + // 2. View dashboard and see the custom app + $viewResponse = $this->actingAs($user1)->get('/dashboard'); + $viewResponse->assertStatus(200); + $viewResponse->assertSee('MY TEST APP'); // Displayed upper case in UI + + // 3. Update personal app + $updateResponse = $this->actingAs($user1) + ->post(route('dashboard.personal-apps.update', $personalApp->id), [ + 'name' => 'Updated Test App', + 'url' => 'https://example.com/updated', + 'color' => '#654321', + 'tag' => 'UpdatedTag', + 'desc' => 'New description' + ]); + + $updateResponse->assertRedirect(route('dashboard')); + $this->assertDatabaseHas('personal_apps', [ + 'id' => $personalApp->id, + 'name' => 'Updated Test App', + 'url' => 'https://example.com/updated', + 'color' => '#654321' + ]); + + // 4. Security: User 2 cannot update User 1's custom app + $hackerResponse = $this->actingAs($user2) + ->post(route('dashboard.personal-apps.update', $personalApp->id), [ + 'name' => 'Hacked App', + 'url' => 'https://hacked.com', + ]); + + // Let's assert a 404 status because user2 doesn't own it (Auth::user()->personalApps()->findOrFail($id) throws ModelNotFoundException which is rendered as 404) + $hackerResponse->assertStatus(404); + + // 5. Delete personal app + $deleteResponse = $this->actingAs($user1) + ->delete(route('dashboard.personal-apps.destroy', $personalApp->id)); + + $deleteResponse->assertRedirect(route('dashboard')); + $this->assertDatabaseMissing('personal_apps', [ + 'id' => $personalApp->id + ]); + } }