Migrate everything to alert system, update user.ts for OIDC updates.
This commit is contained in:
@@ -46,11 +46,19 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
jwks = await response.json();
|
const jwksData = await response.json() as any;
|
||||||
jwksUrl = url;
|
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
|
||||||
break;
|
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) {
|
} catch (error) {
|
||||||
|
logger.error(`JWKS fetch error from ${url}:`, error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,12 +67,16 @@ async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: str
|
|||||||
throw new Error('Failed to fetch JWKS from any URL');
|
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 header = JSON.parse(Buffer.from(idToken.split('.')[0], 'base64').toString());
|
||||||
const keyId = header.kid;
|
const keyId = header.kid;
|
||||||
|
|
||||||
const publicKey = jwks.keys.find((key: any) => key.kid === keyId);
|
const publicKey = jwks.keys.find((key: any) => key.kid === keyId);
|
||||||
if (!publicKey) {
|
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');
|
const {importJWK, jwtVerify} = await import('jose');
|
||||||
@@ -400,8 +412,19 @@ router.get('/oidc/callback', async (req, res) => {
|
|||||||
if (tokenData.id_token) {
|
if (tokenData.id_token) {
|
||||||
try {
|
try {
|
||||||
userInfo = await verifyOIDCToken(tokenData.id_token, config.issuer_url, config.client_id);
|
userInfo = await verifyOIDCToken(tokenData.id_token, config.issuer_url, config.client_id);
|
||||||
|
logger.info('Successfully verified ID token and extracted user info');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('OIDC token verification failed, trying userinfo endpoints', 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) {
|
if (!userInfo) {
|
||||||
logger.error('Failed to get user information from all sources');
|
logger.error('Failed to get user information from all sources');
|
||||||
logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`);
|
logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`);
|
||||||
|
|||||||
@@ -178,34 +178,35 @@ function Sidebar({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
// Commented out mobile behavior to keep sidebar always visible
|
||||||
return (
|
// if (isMobile) {
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
// return (
|
||||||
<SheetContent
|
// <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||||
data-sidebar="sidebar"
|
// <SheetContent
|
||||||
data-slot="sidebar"
|
// data-sidebar="sidebar"
|
||||||
data-mobile="true"
|
// data-slot="sidebar"
|
||||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
// data-mobile="true"
|
||||||
style={
|
// className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
{
|
// style={
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
// {
|
||||||
} as React.CSSProperties
|
// "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
}
|
// } as React.CSSProperties
|
||||||
side={side}
|
// }
|
||||||
>
|
// side={side}
|
||||||
<SheetHeader className="sr-only">
|
// >
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
// <SheetHeader className="sr-only">
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
// <SheetTitle>Sidebar</SheetTitle>
|
||||||
</SheetHeader>
|
// <SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
// </SheetHeader>
|
||||||
</SheetContent>
|
// <div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</Sheet>
|
// </SheetContent>
|
||||||
)
|
// </Sheet>
|
||||||
}
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group peer text-sidebar-foreground hidden md:block"
|
className="group peer text-sidebar-foreground block"
|
||||||
data-state={state}
|
data-state={state}
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
@@ -227,7 +228,7 @@ function Sidebar({
|
|||||||
<div
|
<div
|
||||||
data-slot="sidebar-container"
|
data-slot="sidebar-container"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
"fixed inset-y-0 z-10 flex h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear",
|
||||||
side === "left"
|
side === "left"
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table.tsx";
|
} from "@/components/ui/table.tsx";
|
||||||
import {Shield, Trash2, Users} from "lucide-react";
|
import {Shield, Trash2, Users} from "lucide-react";
|
||||||
|
import {toast} from "sonner";
|
||||||
import {
|
import {
|
||||||
getOIDCConfig,
|
getOIDCConfig,
|
||||||
getRegistrationAllowed,
|
getRegistrationAllowed,
|
||||||
@@ -57,7 +58,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
});
|
});
|
||||||
const [oidcLoading, setOidcLoading] = React.useState(false);
|
const [oidcLoading, setOidcLoading] = React.useState(false);
|
||||||
const [oidcError, setOidcError] = React.useState<string | null>(null);
|
const [oidcError, setOidcError] = React.useState<string | null>(null);
|
||||||
const [oidcSuccess, setOidcSuccess] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState<Array<{
|
const [users, setUsers] = React.useState<Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -69,7 +69,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
const [newAdminUsername, setNewAdminUsername] = React.useState("");
|
const [newAdminUsername, setNewAdminUsername] = React.useState("");
|
||||||
const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
|
const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
|
||||||
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
||||||
const [makeAdminSuccess, setMakeAdminSuccess] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
@@ -121,7 +120,6 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
setOidcError(null);
|
setOidcError(null);
|
||||||
setOidcSuccess(null);
|
|
||||||
|
|
||||||
const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
|
const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
|
||||||
const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]);
|
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");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await updateOIDCConfig(oidcConfig);
|
await updateOIDCConfig(oidcConfig);
|
||||||
setOidcSuccess("OIDC configuration updated successfully!");
|
toast.success("OIDC configuration updated successfully!");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration");
|
setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -151,11 +149,10 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
if (!newAdminUsername.trim()) return;
|
if (!newAdminUsername.trim()) return;
|
||||||
setMakeAdminLoading(true);
|
setMakeAdminLoading(true);
|
||||||
setMakeAdminError(null);
|
setMakeAdminError(null);
|
||||||
setMakeAdminSuccess(null);
|
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await makeUserAdmin(newAdminUsername.trim());
|
await makeUserAdmin(newAdminUsername.trim());
|
||||||
setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`);
|
toast.success(`User ${newAdminUsername} is now an admin`);
|
||||||
setNewAdminUsername("");
|
setNewAdminUsername("");
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -170,9 +167,11 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await removeAdminStatus(username);
|
await removeAdminStatus(username);
|
||||||
|
toast.success(`Admin status removed from ${username}`);
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to remove admin status:', err);
|
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");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await deleteUser(username);
|
await deleteUser(username);
|
||||||
|
toast.success(`User ${username} deleted successfully`);
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to delete user:', err);
|
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: ''
|
userinfo_url: ''
|
||||||
})}>Reset</Button>
|
})}>Reset</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{oidcSuccess && (
|
|
||||||
<Alert>
|
|
||||||
<AlertTitle>Success</AlertTitle>
|
|
||||||
<AlertDescription>{oidcSuccess}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -404,12 +398,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
<AlertDescription>{makeAdminError}</AlertDescription>
|
<AlertDescription>{makeAdminError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{makeAdminSuccess && (
|
|
||||||
<Alert>
|
|
||||||
<AlertTitle>Success</AlertTitle>
|
|
||||||
<AlertDescription>{makeAdminSuccess}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx
|
|||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {Switch} from "@/components/ui/switch.tsx";
|
import {Switch} from "@/components/ui/switch.tsx";
|
||||||
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
|
import {toast} from "sonner";
|
||||||
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
|
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
@@ -244,8 +245,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
|
|
||||||
if (editingHost) {
|
if (editingHost) {
|
||||||
await updateSSHHost(editingHost.id, formData);
|
await updateSSHHost(editingHost.id, formData);
|
||||||
|
toast.success(`Host "${formData.name}" updated successfully!`);
|
||||||
} else {
|
} else {
|
||||||
await createSSHHost(formData);
|
await createSSHHost(formData);
|
||||||
|
toast.success(`Host "${formData.name}" added successfully!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onFormSubmit) {
|
if (onFormSubmit) {
|
||||||
@@ -254,7 +257,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Failed to save host. Please try again.');
|
toast.error('Failed to save host. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {Input} from "@/components/ui/input";
|
|||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
|
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
|
||||||
|
import {toast} from "sonner";
|
||||||
import {
|
import {
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -74,10 +75,11 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
|
|||||||
if (window.confirm(`Are you sure you want to delete "${hostName}"?`)) {
|
if (window.confirm(`Are you sure you want to delete "${hostName}"?`)) {
|
||||||
try {
|
try {
|
||||||
await deleteSSHHost(hostId);
|
await deleteSSHHost(hostId);
|
||||||
|
toast.success(`Host "${hostName}" deleted successfully!`);
|
||||||
await fetchHosts();
|
await fetchHosts();
|
||||||
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
||||||
} catch (err) {
|
} 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);
|
const result = await bulkImportSSHHosts(hostsArray);
|
||||||
|
|
||||||
if (result.success > 0) {
|
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();
|
await fetchHosts();
|
||||||
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
|
||||||
} else {
|
} else {
|
||||||
alert(`Import failed: ${result.errors.join('\n')}`);
|
toast.error(`Import failed: ${result.errors.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to import JSON file';
|
const errorMessage = err instanceof Error ? err.message : 'Failed to import JSON file';
|
||||||
alert(`Import error: ${errorMessage}`);
|
toast.error(`Import error: ${errorMessage}`);
|
||||||
} finally {
|
} finally {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
|
|||||||
@@ -70,13 +70,20 @@ export function Homepage({
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
const topOffset = isTopbarOpen ? 66 : 0;
|
||||||
|
const topPadding = isTopbarOpen ? 66 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-full min-h-svh relative transition-[padding-top] duration-200 ease-linear ${
|
className="w-full min-h-svh relative transition-[padding-top] duration-300 ease-in-out"
|
||||||
isTopbarOpen ? 'pt-[66px]' : 'pt-2'
|
style={{ paddingTop: `${topPadding}px` }}>
|
||||||
}`}>
|
|
||||||
{!loggedIn ? (
|
{!loggedIn ? (
|
||||||
<div className="absolute top-[66px] left-0 w-full h-[calc(100%-66px)] flex items-center justify-center">
|
<div
|
||||||
|
className="absolute left-0 w-full flex items-center justify-center transition-all duration-300 ease-in-out"
|
||||||
|
style={{
|
||||||
|
top: `${topOffset}px`,
|
||||||
|
height: `calc(100% - ${topOffset}px)`
|
||||||
|
}}>
|
||||||
<HomepageAuth
|
<HomepageAuth
|
||||||
setLoggedIn={setLoggedIn}
|
setLoggedIn={setLoggedIn}
|
||||||
setIsAdmin={setIsAdmin}
|
setIsAdmin={setIsAdmin}
|
||||||
@@ -90,8 +97,13 @@ export function Homepage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute top-[66px] left-0 w-full h-[calc(100%-66px)] flex items-center justify-center">
|
<div
|
||||||
<div className="flex flex-row items-center justify-center gap-8 relative z-[10000]">
|
className="absolute left-0 w-full flex items-center justify-center transition-all duration-300 ease-in-out"
|
||||||
|
style={{
|
||||||
|
top: `${topOffset}px`,
|
||||||
|
height: `calc(100% - ${topOffset}px)`
|
||||||
|
}}>
|
||||||
|
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||||
<div
|
<div
|
||||||
className="text-center bg-[#18181b] border-2 border-[#303032] rounded-lg p-6 w-full shadow-lg">
|
className="text-center bg-[#18181b] border-2 border-[#303032] rounded-lg p-6 w-full shadow-lg">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {Label} from "@/components/ui/label.tsx";
|
|||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
interface PasswordResetProps {
|
interface PasswordResetProps {
|
||||||
userInfo: {
|
userInfo: {
|
||||||
@@ -25,7 +26,6 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
|||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [tempToken, setTempToken] = useState("");
|
const [tempToken, setTempToken] = useState("");
|
||||||
const [resetLoading, setResetLoading] = useState(false);
|
const [resetLoading, setResetLoading] = useState(false);
|
||||||
const [resetSuccess, setResetSuccess] = useState(false);
|
|
||||||
|
|
||||||
async function handleInitiatePasswordReset() {
|
async function handleInitiatePasswordReset() {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -48,7 +48,6 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
|||||||
setConfirmPassword("");
|
setConfirmPassword("");
|
||||||
setTempToken("");
|
setTempToken("");
|
||||||
setError(null);
|
setError(null);
|
||||||
setResetSuccess(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleVerifyResetCode() {
|
async function handleVerifyResetCode() {
|
||||||
@@ -85,14 +84,8 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
|||||||
try {
|
try {
|
||||||
await completePasswordReset(userInfo.username, tempToken, newPassword);
|
await completePasswordReset(userInfo.username, tempToken, newPassword);
|
||||||
|
|
||||||
setResetStep("initiate");
|
toast.success("Password reset successfully! You can now log in with your new password.");
|
||||||
setResetCode("");
|
resetPasswordState();
|
||||||
setNewPassword("");
|
|
||||||
setConfirmPassword("");
|
|
||||||
setTempToken("");
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
setResetSuccess(true);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.response?.data?.error || "Failed to complete password reset");
|
setError(err?.response?.data?.error || "Failed to complete password reset");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -120,7 +113,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<>
|
<>
|
||||||
{resetStep === "initiate" && !resetSuccess && (
|
{resetStep === "initiate" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -180,19 +173,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resetSuccess && (
|
{resetStep === "newPassword" && (
|
||||||
<>
|
|
||||||
<Alert className="">
|
|
||||||
<AlertTitle>Success!</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Your password has been successfully reset! You can now log in
|
|
||||||
with your new password.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{resetStep === "newPassword" && !resetSuccess && (
|
|
||||||
<>
|
<>
|
||||||
<div className="text-center text-muted-foreground mb-4">
|
<div className="text-center text-muted-foreground mb-4">
|
||||||
<p>Enter your new password for
|
<p>Enter your new password for
|
||||||
|
|||||||
Reference in New Issue
Block a user