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 (
(null);
- const [oidcSuccess, setOidcSuccess] = React.useState
(null);
const [users, setUsers] = React.useState(null);
- const [makeAdminSuccess, setMakeAdminSuccess] = React.useState(null);
React.useEffect(() => {
const jwt = getCookie("jwt");
@@ -121,7 +120,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
e.preventDefault();
setOidcLoading(true);
setOidcError(null);
- setOidcSuccess(null);
const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]);
@@ -134,7 +132,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
const jwt = getCookie("jwt");
try {
await updateOIDCConfig(oidcConfig);
- setOidcSuccess("OIDC configuration updated successfully!");
+ toast.success("OIDC configuration updated successfully!");
} catch (err: any) {
setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration");
} finally {
@@ -151,11 +149,10 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
if (!newAdminUsername.trim()) return;
setMakeAdminLoading(true);
setMakeAdminError(null);
- setMakeAdminSuccess(null);
const jwt = getCookie("jwt");
try {
await makeUserAdmin(newAdminUsername.trim());
- setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`);
+ toast.success(`User ${newAdminUsername} is now an admin`);
setNewAdminUsername("");
fetchUsers();
} catch (err: any) {
@@ -170,9 +167,11 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
const jwt = getCookie("jwt");
try {
await removeAdminStatus(username);
+ toast.success(`Admin status removed from ${username}`);
fetchUsers();
} catch (err: any) {
console.error('Failed to remove admin status:', err);
+ toast.error('Failed to remove admin status');
}
};
@@ -181,9 +180,11 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
const jwt = getCookie("jwt");
try {
await deleteUser(username);
+ toast.success(`User ${username} deleted successfully`);
fetchUsers();
} catch (err: any) {
console.error('Failed to delete user:', err);
+ toast.error('Failed to delete user');
}
};
@@ -323,13 +324,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
userinfo_url: ''
})}>Reset
-
- {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" && (
<>
)}
- {resetSuccess && (
- <>
-
- Success!
-
- Your password has been successfully reset! You can now log in
- with your new password.
-
-
- >
- )}
-
- {resetStep === "newPassword" && !resetSuccess && (
+ {resetStep === "newPassword" && (
<>
Enter your new password for