authorizedApps(); // Get personal custom apps for this user $personalApps = $user->personalApps()->orderBy('name', 'asc')->get(); return view('dashboard', compact('user', 'services', 'personalApps')); } /** * Launch a service app using SSO. */ public function launchSSO($id) { $user = Auth::user(); $app = \App\Models\App::findOrFail($id); // Check if user is authorized to access this app $authorizedApps = $user->authorizedApps(); if (!$authorizedApps->contains('id', $app->id)) { return redirect()->route('dashboard')->with('error', 'Access denied. You do not have permission to access this service.'); } if ($user->microsoft_id) { // Save target app URL in session so callback knows where to redirect session(['sso_target_url' => $app->url]); try { // Determine domain hint based on user's email domain to skip account type prompt $domainHint = (str_ends_with($user->email, '.onmicrosoft.com') || (str_contains($user->email, '@') && !str_ends_with($user->email, 'outlook.com') && !str_ends_with($user->email, 'hotmail.com') && !str_ends_with($user->email, 'live.com'))) ? 'organizations' : 'consumers'; // Redirect to Microsoft OAuth authorize page with login_hint, domain_hint, prompt=none and custom callback return Socialite::driver('microsoft') ->redirectUrl(route('sso.callback')) ->with([ 'login_hint' => $user->email, 'domain_hint' => $domainHint, 'prompt' => 'none' ]) ->redirect(); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Microsoft SSO launch failed', [ 'message' => $e->getMessage() ]); // Fallback: direct redirect return redirect($app->url); } } // For non-Microsoft or non-Microsoft-linked users, redirect directly return redirect($app->url); } /** * Handle the callback for Microsoft SSO. */ public function handleSSOCallback(Request $request) { // If Microsoft returns an error (user logged out or session expired) if ($request->has('error')) { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('login')->with('error', 'You have been signed out because your Microsoft session is no longer active.'); } $user = Auth::user(); try { $microsoftUser = Socialite::driver('microsoft') ->redirectUrl(route('sso.callback')) ->user(); if ($microsoftUser && $user) { // Update tokens in db $user->update([ 'microsoft_token' => $microsoftUser->token, 'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token, ]); // Update verification timestamp session(['ms_session_last_checked' => now()]); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Microsoft SSO callback failed', [ 'message' => $e->getMessage() ]); // Treat token exchange failure as an expired session Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('login')->with('error', 'Your Microsoft session has expired. Please log in again.'); } // If this request was triggered by VerifyMicrosoftSession middleware, redirect back to origin if (session()->has('sso_check_origin')) { $origin = session('sso_check_origin'); session()->forget('sso_check_origin'); return redirect($origin); } $targetUrl = session('sso_target_url', 'https://outlook.office.com/mail/'); session()->forget('sso_target_url'); // If launching Microsoft Teams, redirect to the deep link launcher page if (str_contains($targetUrl, 'teams.microsoft.com')) { return redirect()->route('sso.launch-app', [ 'name' => 'Microsoft Teams', 'url' => $targetUrl, 'protocol' => 'msteams://' ]); } return redirect($targetUrl); } /** * Render the desktop application launcher page with web fallback. */ public function launchApp(Request $request) { $name = $request->query('name', 'Application'); $url = $request->query('url'); $protocol = $request->query('protocol'); if (!$url || !$protocol) { return redirect()->route('dashboard'); } 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.'); } }