Fix OIDC errors for "Failed to get user information"
This commit is contained in:
@@ -222,6 +222,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
|
|||||||
issuer_url,
|
issuer_url,
|
||||||
authorization_url,
|
authorization_url,
|
||||||
token_url,
|
token_url,
|
||||||
|
userinfo_url,
|
||||||
identifier_path,
|
identifier_path,
|
||||||
name_path,
|
name_path,
|
||||||
scopes
|
scopes
|
||||||
@@ -240,6 +241,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
|
|||||||
issuer_url,
|
issuer_url,
|
||||||
authorization_url,
|
authorization_url,
|
||||||
token_url,
|
token_url,
|
||||||
|
userinfo_url: userinfo_url || '',
|
||||||
identifier_path,
|
identifier_path,
|
||||||
name_path,
|
name_path,
|
||||||
scopes: scopes || 'openid email profile'
|
scopes: scopes || 'openid email profile'
|
||||||
@@ -362,14 +364,38 @@ router.get('/oidc/callback', async (req, res) => {
|
|||||||
const tokenData = await tokenResponse.json() as any;
|
const tokenData = await tokenResponse.json() as any;
|
||||||
|
|
||||||
let userInfo: any = null;
|
let userInfo: any = null;
|
||||||
const userInfoUrls = [];
|
let userInfoUrls: string[] = [];
|
||||||
|
|
||||||
const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url;
|
const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url;
|
||||||
const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '');
|
const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '');
|
||||||
|
|
||||||
userInfoUrls.push(`${baseUrl}/userinfo/`);
|
try {
|
||||||
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo/`);
|
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
|
||||||
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo`);
|
const discoveryResponse = await fetch(discoveryUrl);
|
||||||
|
if (discoveryResponse.ok) {
|
||||||
|
const discovery = await discoveryResponse.json() as any;
|
||||||
|
if (discovery.userinfo_endpoint) {
|
||||||
|
userInfoUrls.push(discovery.userinfo_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (discoveryError) {
|
||||||
|
logger.error(`OIDC discovery failed: ${discoveryError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.userinfo_url) {
|
||||||
|
userInfoUrls.unshift(config.userinfo_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfoUrls.push(
|
||||||
|
`${baseUrl}/userinfo/`,
|
||||||
|
`${baseUrl}/userinfo`,
|
||||||
|
`${normalizedIssuerUrl}/userinfo/`,
|
||||||
|
`${normalizedIssuerUrl}/userinfo`,
|
||||||
|
`${baseUrl}/oauth2/userinfo/`,
|
||||||
|
`${baseUrl}/oauth2/userinfo`,
|
||||||
|
`${normalizedIssuerUrl}/oauth2/userinfo/`,
|
||||||
|
`${normalizedIssuerUrl}/oauth2/userinfo`
|
||||||
|
);
|
||||||
|
|
||||||
if (tokenData.id_token) {
|
if (tokenData.id_token) {
|
||||||
try {
|
try {
|
||||||
@@ -391,15 +417,34 @@ router.get('/oidc/callback', async (req, res) => {
|
|||||||
if (userInfoResponse.ok) {
|
if (userInfoResponse.ok) {
|
||||||
userInfo = await userInfoResponse.json();
|
userInfo = await userInfoResponse.json();
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
logger.error(`Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(`Token data keys: ${Object.keys(tokenData).join(', ')}`);
|
||||||
|
logger.error(`Has id_token: ${!!tokenData.id_token}`);
|
||||||
|
logger.error(`Has access_token: ${!!tokenData.access_token}`);
|
||||||
return res.status(400).json({error: 'Failed to get user information'});
|
return res.status(400).json({error: 'Failed to get user information'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
removeAdminStatus,
|
removeAdminStatus,
|
||||||
deleteUser
|
deleteUser
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
return document.cookie.split('; ').reduce((r, v) => {
|
return document.cookie.split('; ').reduce((r, v) => {
|
||||||
@@ -41,7 +40,6 @@ interface AdminSettingsProps {
|
|||||||
|
|
||||||
export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement {
|
export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement {
|
||||||
const {state: sidebarState} = useSidebar();
|
const {state: sidebarState} = useSidebar();
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
||||||
const [regLoading, setRegLoading] = React.useState(false);
|
const [regLoading, setRegLoading] = React.useState(false);
|
||||||
@@ -137,7 +135,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
await updateOIDCConfig(oidcConfig);
|
await updateOIDCConfig(oidcConfig);
|
||||||
setOidcSuccess("OIDC configuration updated successfully!");
|
setOidcSuccess("OIDC configuration updated successfully!");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setOidcError(err?.response?.data?.error || t('interface.failedToUpdateOidcConfig'));
|
setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration");
|
||||||
} finally {
|
} finally {
|
||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
}
|
}
|
||||||
@@ -160,7 +158,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
setNewAdminUsername("");
|
setNewAdminUsername("");
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMakeAdminError(err?.response?.data?.error || t('interface.failedToMakeUserAdmin'));
|
setMakeAdminError(err?.response?.data?.error || "Failed to make user admin");
|
||||||
} finally {
|
} finally {
|
||||||
setMakeAdminLoading(false);
|
setMakeAdminLoading(false);
|
||||||
}
|
}
|
||||||
@@ -202,7 +200,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
|
className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="h-full w-full flex flex-col">
|
||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
<h1 className="font-bold text-lg">{t('admin.title')}</h1>
|
<h1 className="font-bold text-lg">Admin Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="p-0.25 w-full"/>
|
<Separator className="p-0.25 w-full"/>
|
||||||
|
|
||||||
@@ -211,98 +209,99 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||||
<TabsTrigger value="registration" className="flex items-center gap-2">
|
<TabsTrigger value="registration" className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4"/>
|
<Users className="h-4 w-4"/>
|
||||||
{t('common.settings')}
|
General
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="oidc" className="flex items-center gap-2">
|
<TabsTrigger value="oidc" className="flex items-center gap-2">
|
||||||
<Shield className="h-4 w-4"/>
|
<Shield className="h-4 w-4"/>
|
||||||
{t('admin.oidcSettings')}
|
OIDC
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="users" className="flex items-center gap-2">
|
<TabsTrigger value="users" className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4"/>
|
<Users className="h-4 w-4"/>
|
||||||
{t('admin.users')}
|
Users
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="admins" className="flex items-center gap-2">
|
<TabsTrigger value="admins" className="flex items-center gap-2">
|
||||||
<Shield className="h-4 w-4"/>
|
<Shield className="h-4 w-4"/>
|
||||||
{t('nav.admin')}
|
Admins
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="registration" className="space-y-6">
|
<TabsContent value="registration" className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.userManagement')}</h3>
|
<h3 className="text-lg font-semibold">User Registration</h3>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={allowRegistration} onCheckedChange={handleToggleRegistration}
|
<Checkbox checked={allowRegistration} onCheckedChange={handleToggleRegistration}
|
||||||
disabled={regLoading}/>
|
disabled={regLoading}/>
|
||||||
{t('admin.allowRegistration')}
|
Allow new account registration
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="oidc" className="space-y-6">
|
<TabsContent value="oidc" className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.externalAuthentication')}</h3>
|
<h3 className="text-lg font-semibold">External Authentication (OIDC)</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t('admin.configureExternalProvider')}</p>
|
<p className="text-sm text-muted-foreground">Configure external identity provider for
|
||||||
|
OIDC/OAuth2 authentication.</p>
|
||||||
|
|
||||||
{oidcError && (
|
{oidcError && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{oidcError}</AlertDescription>
|
<AlertDescription>{oidcError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleOIDCConfigSubmit} className="space-y-4">
|
<form onSubmit={handleOIDCConfigSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="client_id">{t('admin.clientId')}</Label>
|
<Label htmlFor="client_id">Client ID</Label>
|
||||||
<Input id="client_id" value={oidcConfig.client_id}
|
<Input id="client_id" value={oidcConfig.client_id}
|
||||||
onChange={(e) => handleOIDCConfigChange('client_id', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('client_id', e.target.value)}
|
||||||
placeholder={t('placeholders.clientId')} required/>
|
placeholder="your-client-id" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>
|
<Label htmlFor="client_secret">Client Secret</Label>
|
||||||
<Input id="client_secret" type="password" value={oidcConfig.client_secret}
|
<Input id="client_secret" type="password" value={oidcConfig.client_secret}
|
||||||
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
|
||||||
placeholder={t('placeholders.clientSecret')} required/>
|
placeholder="your-client-secret" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="authorization_url">{t('admin.authorizationUrl')}</Label>
|
<Label htmlFor="authorization_url">Authorization URL</Label>
|
||||||
<Input id="authorization_url" value={oidcConfig.authorization_url}
|
<Input id="authorization_url" value={oidcConfig.authorization_url}
|
||||||
onChange={(e) => handleOIDCConfigChange('authorization_url', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('authorization_url', e.target.value)}
|
||||||
placeholder={t('placeholders.authUrl')}
|
placeholder="https://your-provider.com/application/o/authorize/"
|
||||||
required/>
|
required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="issuer_url">{t('admin.issuerUrl')}</Label>
|
<Label htmlFor="issuer_url">Issuer URL</Label>
|
||||||
<Input id="issuer_url" value={oidcConfig.issuer_url}
|
<Input id="issuer_url" value={oidcConfig.issuer_url}
|
||||||
onChange={(e) => handleOIDCConfigChange('issuer_url', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('issuer_url', e.target.value)}
|
||||||
placeholder={t('placeholders.redirectUrl')} required/>
|
placeholder="https://your-provider.com/application/o/termix/" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="token_url">{t('admin.tokenUrl')}</Label>
|
<Label htmlFor="token_url">Token URL</Label>
|
||||||
<Input id="token_url" value={oidcConfig.token_url}
|
<Input id="token_url" value={oidcConfig.token_url}
|
||||||
onChange={(e) => handleOIDCConfigChange('token_url', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('token_url', e.target.value)}
|
||||||
placeholder={t('placeholders.tokenUrl')} required/>
|
placeholder="https://your-provider.com/application/o/token/" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="identifier_path">{t('admin.userIdentifierPath')}</Label>
|
<Label htmlFor="identifier_path">User Identifier Path</Label>
|
||||||
<Input id="identifier_path" value={oidcConfig.identifier_path}
|
<Input id="identifier_path" value={oidcConfig.identifier_path}
|
||||||
onChange={(e) => handleOIDCConfigChange('identifier_path', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('identifier_path', e.target.value)}
|
||||||
placeholder={t('placeholders.userIdField')} required/>
|
placeholder="sub" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name_path">{t('admin.displayNamePath')}</Label>
|
<Label htmlFor="name_path">Display Name Path</Label>
|
||||||
<Input id="name_path" value={oidcConfig.name_path}
|
<Input id="name_path" value={oidcConfig.name_path}
|
||||||
onChange={(e) => handleOIDCConfigChange('name_path', e.target.value)}
|
onChange={(e) => handleOIDCConfigChange('name_path', e.target.value)}
|
||||||
placeholder={t('placeholders.usernameField')} required/>
|
placeholder="name" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="scopes">{t('admin.scopes')}</Label>
|
<Label htmlFor="scopes">Scopes</Label>
|
||||||
<Input id="scopes" value={oidcConfig.scopes}
|
<Input id="scopes" value={oidcConfig.scopes}
|
||||||
onChange={(e) => handleOIDCConfigChange('scopes', (e.target as HTMLInputElement).value)}
|
onChange={(e) => handleOIDCConfigChange('scopes', (e.target as HTMLInputElement).value)}
|
||||||
placeholder={t('placeholders.scopes')} required/>
|
placeholder="openid email profile" required/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<Button type="submit" className="flex-1"
|
<Button type="submit" className="flex-1"
|
||||||
disabled={oidcLoading}>{oidcLoading ? t('admin.saving') : t('admin.saveConfiguration')}</Button>
|
disabled={oidcLoading}>{oidcLoading ? "Saving..." : "Save Configuration"}</Button>
|
||||||
<Button type="button" variant="outline" onClick={() => setOidcConfig({
|
<Button type="button" variant="outline" onClick={() => setOidcConfig({
|
||||||
client_id: '',
|
client_id: '',
|
||||||
client_secret: '',
|
client_secret: '',
|
||||||
@@ -312,12 +311,12 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
identifier_path: 'sub',
|
identifier_path: 'sub',
|
||||||
name_path: 'name',
|
name_path: 'name',
|
||||||
scopes: 'openid email profile'
|
scopes: 'openid email profile'
|
||||||
})}>{t('admin.reset')}</Button>
|
})}>Reset</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{oidcSuccess && (
|
{oidcSuccess && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle>{t('admin.success')}</AlertTitle>
|
<AlertTitle>Success</AlertTitle>
|
||||||
<AlertDescription>{oidcSuccess}</AlertDescription>
|
<AlertDescription>{oidcSuccess}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -328,20 +327,20 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
<TabsContent value="users" className="space-y-6">
|
<TabsContent value="users" className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.userManagement')}</h3>
|
<h3 className="text-lg font-semibold">User Management</h3>
|
||||||
<Button onClick={fetchUsers} disabled={usersLoading} variant="outline"
|
<Button onClick={fetchUsers} disabled={usersLoading} variant="outline"
|
||||||
size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
|
size="sm">{usersLoading ? "Loading..." : "Refresh"}</Button>
|
||||||
</div>
|
</div>
|
||||||
{usersLoading ? (
|
{usersLoading ? (
|
||||||
<div className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
|
<div className="text-center py-8 text-muted-foreground">Loading users...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="border rounded-md overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
<TableHead className="px-4">Username</TableHead>
|
||||||
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
<TableHead className="px-4">Type</TableHead>
|
||||||
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
<TableHead className="px-4">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -351,11 +350,11 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
{user.username}
|
{user.username}
|
||||||
{user.is_admin && (
|
{user.is_admin && (
|
||||||
<span
|
<span
|
||||||
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')}</span>
|
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">Admin</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="px-4">{user.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
className="px-4">{user.is_oidc ? "External" : "Local"}</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell className="px-4">
|
||||||
<Button variant="ghost" size="sm"
|
<Button variant="ghost" size="sm"
|
||||||
onClick={() => deleteUser(user.username)}
|
onClick={() => deleteUser(user.username)}
|
||||||
@@ -375,29 +374,29 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
|
|
||||||
<TabsContent value="admins" className="space-y-6">
|
<TabsContent value="admins" className="space-y-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.adminManagement')}</h3>
|
<h3 className="text-lg font-semibold">Admin Management</h3>
|
||||||
<div className="space-y-4 p-6 border rounded-md bg-muted/50">
|
<div className="space-y-4 p-6 border rounded-md bg-muted/50">
|
||||||
<h4 className="font-medium">{t('admin.makeUserAdmin')}</h4>
|
<h4 className="font-medium">Make User Admin</h4>
|
||||||
<form onSubmit={makeUserAdmin} className="space-y-4">
|
<form onSubmit={makeUserAdmin} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="new-admin-username">{t('admin.username')}</Label>
|
<Label htmlFor="new-admin-username">Username</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input id="new-admin-username" value={newAdminUsername}
|
<Input id="new-admin-username" value={newAdminUsername}
|
||||||
onChange={(e) => setNewAdminUsername(e.target.value)}
|
onChange={(e) => setNewAdminUsername(e.target.value)}
|
||||||
placeholder={t('placeholders.enterUsername')} required/>
|
placeholder="Enter username to make admin" required/>
|
||||||
<Button type="submit"
|
<Button type="submit"
|
||||||
disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? t('admin.adding') : t('admin.makeAdmin')}</Button>
|
disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? "Adding..." : "Make Admin"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{makeAdminError && (
|
{makeAdminError && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{makeAdminError}</AlertDescription>
|
<AlertDescription>{makeAdminError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{makeAdminSuccess && (
|
{makeAdminSuccess && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle>{t('admin.success')}</AlertTitle>
|
<AlertTitle>Success</AlertTitle>
|
||||||
<AlertDescription>{makeAdminSuccess}</AlertDescription>
|
<AlertDescription>{makeAdminSuccess}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -405,14 +404,14 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="font-medium">{t('admin.currentAdmins')}</h4>
|
<h4 className="font-medium">Current Admins</h4>
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="border rounded-md overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
<TableHead className="px-4">Username</TableHead>
|
||||||
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
<TableHead className="px-4">Type</TableHead>
|
||||||
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
<TableHead className="px-4">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -424,13 +423,13 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">Admin</span>
|
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">Admin</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
className="px-4">{admin.is_oidc ? "External" : "Local"}</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell className="px-4">
|
||||||
<Button variant="ghost" size="sm"
|
<Button variant="ghost" size="sm"
|
||||||
onClick={() => removeAdminStatus(admin.username)}
|
onClick={() => removeAdminStatus(admin.username)}
|
||||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
|
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
|
||||||
<Shield className="h-4 w-4"/>
|
<Shield className="h-4 w-4"/>
|
||||||
{t('admin.removeAdminButton')}
|
Remove Admin
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import {useTranslation} from 'react-i18next';
|
|
||||||
import {
|
import {
|
||||||
Computer,
|
Computer,
|
||||||
Server,
|
Server,
|
||||||
@@ -113,7 +112,6 @@ export function LeftSidebar({
|
|||||||
username,
|
username,
|
||||||
children,
|
children,
|
||||||
}: SidebarProps): React.ReactElement {
|
}: SidebarProps): React.ReactElement {
|
||||||
const {t} = useTranslation();
|
|
||||||
const [adminSheetOpen, setAdminSheetOpen] = React.useState(false);
|
const [adminSheetOpen, setAdminSheetOpen] = React.useState(false);
|
||||||
|
|
||||||
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
|
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
|
||||||
@@ -142,7 +140,7 @@ export function LeftSidebar({
|
|||||||
const sshManagerTab = tabList.find((t) => t.type === 'ssh_manager');
|
const sshManagerTab = tabList.find((t) => t.type === 'ssh_manager');
|
||||||
const openSshManagerTab = () => {
|
const openSshManagerTab = () => {
|
||||||
if (sshManagerTab || isSplitScreenActive) return;
|
if (sshManagerTab || isSplitScreenActive) return;
|
||||||
const id = addTab({type: 'ssh_manager', title: t('hosts.title')} as any);
|
const id = addTab({type: 'ssh_manager', title: 'SSH Manager'} as any);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
};
|
};
|
||||||
const adminTab = tabList.find((t) => t.type === 'admin');
|
const adminTab = tabList.find((t) => t.type === 'admin');
|
||||||
@@ -152,7 +150,7 @@ export function LeftSidebar({
|
|||||||
setCurrentTab(adminTab.id);
|
setCurrentTab(adminTab.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = addTab({type: 'admin', title: t('nav.admin')} as any);
|
const id = addTab({type: 'admin', title: 'Admin'} as any);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,7 +232,7 @@ export function LeftSidebar({
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setHostsError(t('leftSidebar.failedToLoadHosts'));
|
setHostsError('Failed to load hosts');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -277,7 +275,7 @@ export function LeftSidebar({
|
|||||||
const hostsByFolder = React.useMemo(() => {
|
const hostsByFolder = React.useMemo(() => {
|
||||||
const map: Record<string, SSHHost[]> = {};
|
const map: Record<string, SSHHost[]> = {};
|
||||||
filteredHosts.forEach(h => {
|
filteredHosts.forEach(h => {
|
||||||
const folder = h.folder && h.folder.trim() ? h.folder : t('leftSidebar.noFolder');
|
const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder';
|
||||||
if (!map[folder]) map[folder] = [];
|
if (!map[folder]) map[folder] = [];
|
||||||
map[folder].push(h);
|
map[folder].push(h);
|
||||||
});
|
});
|
||||||
@@ -287,8 +285,8 @@ export function LeftSidebar({
|
|||||||
const sortedFolders = React.useMemo(() => {
|
const sortedFolders = React.useMemo(() => {
|
||||||
const folders = Object.keys(hostsByFolder);
|
const folders = Object.keys(hostsByFolder);
|
||||||
folders.sort((a, b) => {
|
folders.sort((a, b) => {
|
||||||
if (a === t('leftSidebar.noFolder')) return -1;
|
if (a === 'No Folder') return -1;
|
||||||
if (b === t('leftSidebar.noFolder')) return 1;
|
if (b === 'No Folder') return 1;
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
return folders;
|
return folders;
|
||||||
@@ -306,7 +304,7 @@ export function LeftSidebar({
|
|||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
|
|
||||||
if (!deletePassword.trim()) {
|
if (!deletePassword.trim()) {
|
||||||
setDeleteError(t('leftSidebar.passwordRequired'));
|
setDeleteError("Password is required");
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -317,7 +315,7 @@ export function LeftSidebar({
|
|||||||
|
|
||||||
handleLogout();
|
handleLogout();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setDeleteError(err?.response?.data?.error || t('leftSidebar.failedToDeleteAccount'));
|
setDeleteError(err?.response?.data?.error || "Failed to delete account");
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -372,18 +370,18 @@ export function LeftSidebar({
|
|||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await makeUserAdmin(newAdminUsername.trim());
|
await makeUserAdmin(newAdminUsername.trim());
|
||||||
setMakeAdminSuccess(t('leftSidebar.userIsNowAdmin', {username: newAdminUsername}));
|
setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`);
|
||||||
setNewAdminUsername("");
|
setNewAdminUsername("");
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMakeAdminError(err?.response?.data?.error || t('leftSidebar.failedToMakeUserAdmin'));
|
setMakeAdminError(err?.response?.data?.error || "Failed to make user admin");
|
||||||
} finally {
|
} finally {
|
||||||
setMakeAdminLoading(false);
|
setMakeAdminLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeAdminStatus = async (username: string) => {
|
const removeAdminStatus = async (username: string) => {
|
||||||
if (!confirm(t('leftSidebar.removeAdminConfirm', {username}))) return;
|
if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return;
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return;
|
return;
|
||||||
@@ -398,7 +396,7 @@ export function LeftSidebar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = async (username: string) => {
|
const deleteUser = async (username: string) => {
|
||||||
if (!confirm(t('leftSidebar.deleteUserConfirm', {username}))) return;
|
if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return;
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return;
|
return;
|
||||||
@@ -433,9 +431,9 @@ export function LeftSidebar({
|
|||||||
<SidebarGroup className="!m-0 !p-0 !-mb-2">
|
<SidebarGroup className="!m-0 !p-0 !-mb-2">
|
||||||
<Button className="m-2 flex flex-row font-semibold border-2 !border-[#303032]" variant="outline"
|
<Button className="m-2 flex flex-row font-semibold border-2 !border-[#303032]" variant="outline"
|
||||||
onClick={openSshManagerTab} disabled={!!sshManagerTab || isSplitScreenActive}
|
onClick={openSshManagerTab} disabled={!!sshManagerTab || isSplitScreenActive}
|
||||||
title={sshManagerTab ? t('interface.sshManagerAlreadyOpen') : isSplitScreenActive ? t('interface.disabledDuringSplitScreen') : undefined}>
|
title={sshManagerTab ? 'SSH Manager already open' : isSplitScreenActive ? 'Disabled during split screen' : undefined}>
|
||||||
<HardDrive strokeWidth="2.5"/>
|
<HardDrive strokeWidth="2.5"/>
|
||||||
{t('nav.hostManager')}
|
Host Manager
|
||||||
</Button>
|
</Button>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<Separator className="p-0.25"/>
|
<Separator className="p-0.25"/>
|
||||||
@@ -444,7 +442,7 @@ export function LeftSidebar({
|
|||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
placeholder={t('placeholders.searchHostsAny')}
|
placeholder="Search hosts by any info..."
|
||||||
className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md"
|
className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -462,7 +460,7 @@ export function LeftSidebar({
|
|||||||
{hostsLoading && (
|
{hostsLoading && (
|
||||||
<div className="px-4 pb-2">
|
<div className="px-4 pb-2">
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
{t('common.loading')}
|
Loading hosts...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -489,7 +487,7 @@ export function LeftSidebar({
|
|||||||
style={{width: '100%'}}
|
style={{width: '100%'}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<User2/> {username ? username : t('common.logout')}
|
<User2/> {username ? username : 'Signed out'}
|
||||||
<ChevronUp className="ml-auto"/>
|
<ChevronUp className="ml-auto"/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -508,10 +506,10 @@ export function LeftSidebar({
|
|||||||
setCurrentTab(profileTab.id);
|
setCurrentTab(profileTab.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = addTab({type: 'profile', title: t('common.profile')} as any);
|
const id = addTab({type: 'profile', title: 'Profile'} as any);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
}}>
|
}}>
|
||||||
<span>{t('common.profile')}</span>
|
<span>Profile & Security</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -519,13 +517,13 @@ export function LeftSidebar({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAdmin) openAdminTab();
|
if (isAdmin) openAdminTab();
|
||||||
}}>
|
}}>
|
||||||
<span>{t('admin.title')}</span>
|
<span>Admin Settings</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
|
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
|
||||||
onClick={handleLogout}>
|
onClick={handleLogout}>
|
||||||
<span>{t('common.logout')}</span>
|
<span>Sign out</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
|
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
|
||||||
@@ -534,7 +532,7 @@ export function LeftSidebar({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={isAdmin && adminCount <= 1 ? "text-muted-foreground" : "text-red-400"}>
|
className={isAdmin && adminCount <= 1 ? "text-muted-foreground" : "text-red-400"}>
|
||||||
{t('admin.deleteUser')}
|
Delete Account
|
||||||
{isAdmin && adminCount <= 1 && " (Last Admin)"}
|
{isAdmin && adminCount <= 1 && " (Last Admin)"}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -588,7 +586,7 @@ export function LeftSidebar({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-4 border-b border-[#303032]">
|
<div className="flex items-center justify-between p-4 border-b border-[#303032]">
|
||||||
<h2 className="text-lg font-semibold text-white">{t('leftSidebar.deleteAccount')}</h2>
|
<h2 className="text-lg font-semibold text-white">Delete Account</h2>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -598,7 +596,7 @@ export function LeftSidebar({
|
|||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
}}
|
}}
|
||||||
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
|
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
|
||||||
title={t('leftSidebar.closeDeleteAccount')}
|
title="Close Delete Account"
|
||||||
>
|
>
|
||||||
<span className="text-lg font-bold leading-none">×</span>
|
<span className="text-lg font-bold leading-none">×</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -607,19 +605,22 @@ export function LeftSidebar({
|
|||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm text-gray-300">
|
<div className="text-sm text-gray-300">
|
||||||
{t('leftSidebar.deleteAccountWarning')}
|
This action cannot be undone. This will permanently delete your account and all
|
||||||
|
associated data.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>{t('common.warning')}</AlertTitle>
|
<AlertTitle>Warning</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t('leftSidebar.deleteAccountWarningDetails')}
|
Deleting your account will remove all your data including SSH hosts,
|
||||||
|
configurations, and settings.
|
||||||
|
This action is irreversible.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{deleteError && (
|
{deleteError && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{deleteError}</AlertDescription>
|
<AlertDescription>{deleteError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -627,21 +628,23 @@ export function LeftSidebar({
|
|||||||
<form onSubmit={handleDeleteAccount} className="space-y-4">
|
<form onSubmit={handleDeleteAccount} className="space-y-4">
|
||||||
{isAdmin && adminCount <= 1 && (
|
{isAdmin && adminCount <= 1 && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertTitle>{t('leftSidebar.cannotDeleteAccount')}</AlertTitle>
|
<AlertTitle>Cannot Delete Account</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t('leftSidebar.lastAdminWarning')}
|
You are the last admin user. You cannot delete your account as this
|
||||||
|
would leave the system without any administrators.
|
||||||
|
Please make another user an admin first, or contact system support.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="delete-password">{t('leftSidebar.confirmPassword')}</Label>
|
<Label htmlFor="delete-password">Confirm Password</Label>
|
||||||
<Input
|
<Input
|
||||||
id="delete-password"
|
id="delete-password"
|
||||||
type="password"
|
type="password"
|
||||||
value={deletePassword}
|
value={deletePassword}
|
||||||
onChange={(e) => setDeletePassword(e.target.value)}
|
onChange={(e) => setDeletePassword(e.target.value)}
|
||||||
placeholder={t('placeholders.confirmPassword')}
|
placeholder="Enter your password to confirm"
|
||||||
required
|
required
|
||||||
disabled={isAdmin && adminCount <= 1}
|
disabled={isAdmin && adminCount <= 1}
|
||||||
/>
|
/>
|
||||||
@@ -654,7 +657,7 @@ export function LeftSidebar({
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={deleteLoading || !deletePassword.trim() || (isAdmin && adminCount <= 1)}
|
disabled={deleteLoading || !deletePassword.trim() || (isAdmin && adminCount <= 1)}
|
||||||
>
|
>
|
||||||
{deleteLoading ? t('leftSidebar.deleting') : t('leftSidebar.deleteAccount')}
|
{deleteLoading ? "Deleting..." : "Delete Account"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -665,7 +668,7 @@ export function LeftSidebar({
|
|||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('leftSidebar.cancel')}
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ interface UserInfo {
|
|||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
|
is_oidc: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserCount {
|
interface UserCount {
|
||||||
@@ -897,7 +898,7 @@ export async function setupTOTP(): Promise<{ secret: string; qr_code: string }>
|
|||||||
const response = await authApi.post('/users/totp/setup');
|
const response = await authApi.post('/users/totp/setup');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error as AxiosError);
|
handleApiError(error as AxiosError, 'setup TOTP');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,7 +908,7 @@ export async function enableTOTP(totp_code: string): Promise<{ message: string;
|
|||||||
const response = await authApi.post('/users/totp/enable', { totp_code });
|
const response = await authApi.post('/users/totp/enable', { totp_code });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error as AxiosError);
|
handleApiError(error as AxiosError, 'enable TOTP');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -917,7 +918,7 @@ export async function disableTOTP(password?: string, totp_code?: string): Promis
|
|||||||
const response = await authApi.post('/users/totp/disable', { password, totp_code });
|
const response = await authApi.post('/users/totp/disable', { password, totp_code });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error as AxiosError);
|
handleApiError(error as AxiosError, 'disable TOTP');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,7 +928,7 @@ export async function verifyTOTPLogin(temp_token: string, totp_code: string): Pr
|
|||||||
const response = await authApi.post('/users/totp/verify-login', { temp_token, totp_code });
|
const response = await authApi.post('/users/totp/verify-login', { temp_token, totp_code });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error as AxiosError);
|
handleApiError(error as AxiosError, 'verify TOTP login');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,7 +938,7 @@ export async function generateBackupCodes(password?: string, totp_code?: string)
|
|||||||
const response = await authApi.post('/users/totp/backup-codes', { password, totp_code });
|
const response = await authApi.post('/users/totp/backup-codes', { password, totp_code });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error as AxiosError);
|
handleApiError(error as AxiosError, 'generate backup codes');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user