- Added support for optional tenant IDs in JWT claims for user permissions. - Updated auth routes to include tenant information in the JWT payload. - Enhanced app switcher configuration to handle tenant IDs without exposing them publicly. - Improved documentation for EvoBGP tenant ID integration and its usage in JWT.
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
/**
|
|
* Admin editor for portal App Switcher URLs.
|
|
* Preview: https://reui.io/preview/base/settings-16 · https://reui.io/preview/base/settings-3
|
|
*/
|
|
import { useEffect } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import {
|
|
APP_IDS,
|
|
appSwitcherConfigSchema,
|
|
type AppId,
|
|
type AppSwitcherConfig,
|
|
type AppSwitcherIconName,
|
|
} from '@authportal/shared'
|
|
import { Button } from '@authportal/ui/components/button'
|
|
import { Field, FieldGroup, FieldLabel } from '@authportal/ui/components/field'
|
|
import { Input } from '@authportal/ui/components/input'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@authportal/ui/components/select'
|
|
import { Switch } from '@authportal/ui/components/switch'
|
|
import { ItemSeparator } from '@authportal/ui/components/item'
|
|
|
|
const ICON_OPTIONS: AppSwitcherIconName[] = [
|
|
'server',
|
|
'cloud',
|
|
'globe',
|
|
'dashboard',
|
|
'chart',
|
|
]
|
|
|
|
interface AppSwitcherAdminEditorProps {
|
|
defaultValues: AppSwitcherConfig
|
|
onSave: (values: AppSwitcherConfig) => void
|
|
isSaving?: boolean
|
|
}
|
|
|
|
export function AppSwitcherAdminEditor({
|
|
defaultValues,
|
|
onSave,
|
|
isSaving,
|
|
}: AppSwitcherAdminEditorProps) {
|
|
const form = useForm({
|
|
resolver: zodResolver(appSwitcherConfigSchema),
|
|
defaultValues,
|
|
})
|
|
|
|
useEffect(() => {
|
|
form.reset(defaultValues)
|
|
}, [defaultValues, form])
|
|
|
|
return (
|
|
<form
|
|
className="flex flex-col gap-5"
|
|
onSubmit={(e) =>
|
|
void form.handleSubmit((values) => onSave(values))(e)
|
|
}
|
|
>
|
|
<FieldGroup>
|
|
<Field>
|
|
<FieldLabel htmlFor="menu-label">Заголовок меню</FieldLabel>
|
|
<Input id="menu-label" {...form.register('menuLabel')} />
|
|
</Field>
|
|
|
|
{APP_IDS.map((appId, index) => {
|
|
const apps = form.watch('apps')
|
|
const appIndex = apps.findIndex((a) => a.id === appId)
|
|
if (appIndex < 0) return null
|
|
return (
|
|
<div key={appId} className="flex flex-col gap-3">
|
|
{index > 0 ? <ItemSeparator /> : null}
|
|
<p className="text-sm font-medium tracking-tight">
|
|
{appId.toUpperCase()}
|
|
</p>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<Field className="sm:col-span-2">
|
|
<FieldLabel htmlFor={`name-${appId}`}>Название</FieldLabel>
|
|
<Input
|
|
id={`name-${appId}`}
|
|
{...form.register(`apps.${appIndex}.name`)}
|
|
/>
|
|
</Field>
|
|
<Field className="sm:col-span-2">
|
|
<FieldLabel htmlFor={`url-${appId}`}>URL</FieldLabel>
|
|
<Input
|
|
id={`url-${appId}`}
|
|
{...form.register(`apps.${appIndex}.url`)}
|
|
placeholder="https://…"
|
|
/>
|
|
</Field>
|
|
{appId === 'bgp' ? (
|
|
<Field className="sm:col-span-2">
|
|
<FieldLabel htmlFor={`tenant-${appId}`}>
|
|
EvoBGP tenant ID
|
|
</FieldLabel>
|
|
<Input
|
|
id={`tenant-${appId}`}
|
|
{...form.register(`apps.${appIndex}.tenantId`)}
|
|
placeholder="UUID тенанта из EvoBGP"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">
|
|
Попадает в JWT (`bgp_tenant_id` / `tenants.bgp`). Публичный
|
|
switcher его не отдаёт.
|
|
</p>
|
|
</Field>
|
|
) : null}
|
|
<Field className="sm:col-span-2">
|
|
<FieldLabel htmlFor={`subtitle-${appId}`}>
|
|
Описание
|
|
</FieldLabel>
|
|
<Input
|
|
id={`subtitle-${appId}`}
|
|
{...form.register(`apps.${appIndex}.subtitle`)}
|
|
/>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor={`icon-${appId}`}>Иконка</FieldLabel>
|
|
<Select
|
|
value={form.watch(`apps.${appIndex}.icon`)}
|
|
onValueChange={(v) =>
|
|
form.setValue(
|
|
`apps.${appIndex}.icon`,
|
|
(v ?? 'server') as AppSwitcherIconName,
|
|
{ shouldDirty: true },
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger id={`icon-${appId}`}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{ICON_OPTIONS.map((icon) => (
|
|
<SelectItem key={icon} value={icon}>
|
|
{icon}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</Field>
|
|
<Field orientation="horizontal" className="items-center pt-6">
|
|
<Switch
|
|
checked={form.watch(`apps.${appIndex}.enabled`) !== false}
|
|
onCheckedChange={(v) =>
|
|
form.setValue(`apps.${appIndex}.enabled`, v, {
|
|
shouldDirty: true,
|
|
})
|
|
}
|
|
/>
|
|
<FieldLabel className="font-normal">Включено в switcher</FieldLabel>
|
|
</Field>
|
|
</div>
|
|
<input
|
|
type="hidden"
|
|
{...form.register(`apps.${appIndex}.id`)}
|
|
value={appId satisfies AppId}
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
</FieldGroup>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-fit"
|
|
disabled={isSaving || !form.formState.isDirty}
|
|
>
|
|
{isSaving ? 'Сохранение…' : 'Сохранить'}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|