diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 3456aee3..e976da89 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -46,11 +46,19 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str try { const response = await fetch(url); if (response.ok) { - jwks = await response.json(); - jwksUrl = url; - break; + const jwksData = await response.json() as any; + if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { + jwks = jwksData; + jwksUrl = url; + break; + } else { + logger.error(`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`); + } + } else { + logger.error(`JWKS fetch failed from ${url}: ${response.status} ${response.statusText}`); } } catch (error) { + logger.error(`JWKS fetch error from ${url}:`, error); continue; } } @@ -59,12 +67,16 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str throw new Error('Failed to fetch JWKS from any URL'); } + if (!jwks.keys || !Array.isArray(jwks.keys)) { + throw new Error(`Invalid JWKS response structure. Expected 'keys' array, got: ${JSON.stringify(jwks)}`); + } + const header = JSON.parse(Buffer.from(idToken.split('.')[0], 'base64').toString()); const keyId = header.kid; const publicKey = jwks.keys.find((key: any) => key.kid === keyId); if (!publicKey) { - throw new Error(`No matching public key found for key ID: ${keyId}`); + throw new Error(`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(', ')}`); } const {importJWK, jwtVerify} = await import('jose'); @@ -400,8 +412,19 @@ router.get('/oidc/callback', async (req, res) => { if (tokenData.id_token) { try { userInfo = await verifyOIDCToken(tokenData.id_token, config.issuer_url, config.client_id); + logger.info('Successfully verified ID token and extracted user info'); } catch (error) { logger.error('OIDC token verification failed, trying userinfo endpoints', error); + try { + const parts = tokenData.id_token.split('.'); + if (parts.length === 3) { + const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); + userInfo = payload; + logger.info('Successfully decoded ID token payload without verification'); + } + } catch (decodeError) { + logger.error('Failed to decode ID token payload:', decodeError); + } } } @@ -427,18 +450,6 @@ router.get('/oidc/callback', async (req, res) => { } } - if (!userInfo && tokenData.id_token) { - try { - const parts = tokenData.id_token.split('.'); - if (parts.length === 3) { - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); - userInfo = payload; - } - } catch (error) { - logger.error('Failed to decode ID token payload:', error); - } - } - if (!userInfo) { logger.error('Failed to get user information from all sources'); logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`); diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index fe6a02e8..291c11b2 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -178,34 +178,35 @@ function Sidebar({ ) } - if (isMobile) { - return ( - - - - Sidebar - Displays the mobile sidebar. - -
{children}
-
-
- ) - } + // Commented out mobile behavior to keep sidebar always visible + // if (isMobile) { + // return ( + // + // + // + // Sidebar + // Displays the mobile sidebar. + // + //
{children}
+ //
+ //
+ // ) + // } return ( - - {oidcSuccess && ( - - Success - {oidcSuccess} - - )} @@ -404,12 +398,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. {makeAdminError} )} - {makeAdminSuccess && ( - - Success - {makeAdminSuccess} - - )} + diff --git a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx b/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx index 7dc84072..e4e4ccd1 100644 --- a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx +++ b/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx @@ -19,6 +19,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx import React, {useEffect, useRef, useState} from "react"; import {Switch} from "@/components/ui/switch.tsx"; import {Alert, AlertDescription} from "@/components/ui/alert.tsx"; +import {toast} from "sonner"; import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts'; interface SSHHost { @@ -244,8 +245,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos if (editingHost) { await updateSSHHost(editingHost.id, formData); + toast.success(`Host "${formData.name}" updated successfully!`); } else { await createSSHHost(formData); + toast.success(`Host "${formData.name}" added successfully!`); } if (onFormSubmit) { @@ -254,7 +257,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (error) { - alert('Failed to save host. Please try again.'); + toast.error('Failed to save host. Please try again.'); } }; diff --git a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx index 476eb895..61942489 100644 --- a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx +++ b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx @@ -7,6 +7,7 @@ import {Input} from "@/components/ui/input"; import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts"; +import {toast} from "sonner"; import { Edit, Trash2, @@ -74,10 +75,11 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { if (window.confirm(`Are you sure you want to delete "${hostName}"?`)) { try { await deleteSSHHost(hostId); + toast.success(`Host "${hostName}" deleted successfully!`); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (err) { - alert('Failed to delete host'); + toast.error('Failed to delete host'); } } }; @@ -114,16 +116,19 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { const result = await bulkImportSSHHosts(hostsArray); if (result.success > 0) { - alert(`Import completed: ${result.success} successful, ${result.failed} failed${result.errors.length > 0 ? '\n\nErrors:\n' + result.errors.join('\n') : ''}`); + toast.success(`Import completed: ${result.success} hosts imported successfully${result.failed > 0 ? `, ${result.failed} failed` : ''}`); + if (result.errors.length > 0) { + toast.error(`Import errors: ${result.errors.join(', ')}`); + } await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } else { - alert(`Import failed: ${result.errors.join('\n')}`); + toast.error(`Import failed: ${result.errors.join(', ')}`); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to import JSON file'; - alert(`Import error: ${errorMessage}`); + toast.error(`Import error: ${errorMessage}`); } finally { setImporting(false); event.target.value = ''; diff --git a/src/ui/Homepage/Homepage.tsx b/src/ui/Homepage/Homepage.tsx index b6fbe9ae..03854439 100644 --- a/src/ui/Homepage/Homepage.tsx +++ b/src/ui/Homepage/Homepage.tsx @@ -70,13 +70,20 @@ export function Homepage({ } }, [isAuthenticated]); + const topOffset = isTopbarOpen ? 66 : 0; + const topPadding = isTopbarOpen ? 66 : 0; + return (
+ className="w-full min-h-svh relative transition-[padding-top] duration-300 ease-in-out" + style={{ paddingTop: `${topPadding}px` }}> {!loggedIn ? ( -
+
) : ( -
-
+
+
diff --git a/src/ui/User/PasswordReset.tsx b/src/ui/User/PasswordReset.tsx index 19fb4724..df8b8db1 100644 --- a/src/ui/User/PasswordReset.tsx +++ b/src/ui/User/PasswordReset.tsx @@ -6,6 +6,7 @@ import {Label} from "@/components/ui/label.tsx"; import {Input} from "@/components/ui/input.tsx"; import {Button} from "@/components/ui/button.tsx"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; +import {toast} from "sonner"; interface PasswordResetProps { userInfo: { @@ -25,7 +26,6 @@ export function PasswordReset({userInfo}: PasswordResetProps) { const [confirmPassword, setConfirmPassword] = useState(""); const [tempToken, setTempToken] = useState(""); const [resetLoading, setResetLoading] = useState(false); - const [resetSuccess, setResetSuccess] = useState(false); async function handleInitiatePasswordReset() { setError(null); @@ -48,7 +48,6 @@ export function PasswordReset({userInfo}: PasswordResetProps) { setConfirmPassword(""); setTempToken(""); setError(null); - setResetSuccess(false); } async function handleVerifyResetCode() { @@ -85,14 +84,8 @@ export function PasswordReset({userInfo}: PasswordResetProps) { try { await completePasswordReset(userInfo.username, tempToken, newPassword); - setResetStep("initiate"); - setResetCode(""); - setNewPassword(""); - setConfirmPassword(""); - setTempToken(""); - setError(null); - - setResetSuccess(true); + toast.success("Password reset successfully! You can now log in with your new password."); + resetPasswordState(); } catch (err: any) { setError(err?.response?.data?.error || "Failed to complete password reset"); } finally { @@ -120,7 +113,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) { <> - {resetStep === "initiate" && !resetSuccess && ( + {resetStep === "initiate" && ( <>