fix idp on started
This commit is contained in:
@@ -1,20 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Chrome, Loader2, Save, Settings2, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import { Chrome, Loader2, Save, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { AdminShell } from '@/components/id/admin-shell';
|
||||
import { usePublicSettings } from '@/components/id/public-settings-provider';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { apiFetch, buildSystemSettingPayload, SocialProvider, SystemSetting } from '@/lib/api';
|
||||
import { getSettingMeta, isSettingVisible, MESSAGING_SETTING_KEYS, sortSettingsByCatalog, SYSTEM_SETTING_GROUPS } from '@/lib/system-settings-catalog';
|
||||
import { MessagingSettingsSection } from '@/components/id/messaging-settings-section';
|
||||
import { SettingsFieldRow } from '@/components/id/settings-field-row';
|
||||
import { apiFetch, buildSystemSettingPayload, SocialProvider, SystemSetting } from '@/lib/api';
|
||||
import {
|
||||
getSettingMeta,
|
||||
isSettingVisible,
|
||||
MESSAGING_SETTING_KEYS,
|
||||
sortSettingsByCatalog,
|
||||
SYSTEM_SETTING_GROUPS
|
||||
} from '@/lib/system-settings-catalog';
|
||||
|
||||
function parseBoolean(value: string) {
|
||||
return ['true', '1', 'yes'].includes(value.trim().toLowerCase());
|
||||
}
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
...SYSTEM_SETTING_GROUPS,
|
||||
'messaging-email': 'Email (SMTP)',
|
||||
'messaging-sms': 'SMS'
|
||||
};
|
||||
|
||||
const PUBLIC_SETTINGS_REFRESH_KEYS = new Set(['PROJECT_NAME', 'PROJECT_TAGLINE', 'LDAP_ENABLED', 'LDAP_USE_LDAPS']);
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
const { token, user } = useAuth();
|
||||
@@ -23,7 +34,8 @@ export default function AdminSettingsPage() {
|
||||
const [settings, setSettings] = useState<SystemSetting[]>([]);
|
||||
const [providers, setProviders] = useState<SocialProvider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savingKey, setSavingKey] = useState<string | null>(null);
|
||||
const [savingGroup, setSavingGroup] = useState<string | null>(null);
|
||||
const [savingOAuthProvider, setSavingOAuthProvider] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !user?.canManageSettings) return;
|
||||
@@ -52,27 +64,37 @@ export default function AdminSettingsPage() {
|
||||
return groups;
|
||||
}, [settings]);
|
||||
|
||||
async function saveSetting(setting: SystemSetting) {
|
||||
async function saveSettingsGroup(groupKey: string, groupSettings: SystemSetting[]) {
|
||||
if (!token) return;
|
||||
setSavingKey(setting.key);
|
||||
|
||||
const visibleSettings = groupSettings.filter((setting) => isSettingVisible(setting, settings));
|
||||
if (visibleSettings.length === 0) return;
|
||||
|
||||
setSavingGroup(groupKey);
|
||||
try {
|
||||
const updated = await apiFetch<SystemSetting>(
|
||||
'/admin/settings',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(buildSystemSettingPayload(setting))
|
||||
},
|
||||
token
|
||||
);
|
||||
setSettings((current) => sortSettingsByCatalog(current.map((item) => (item.key === updated.key ? updated : item))));
|
||||
if (updated.key === 'PROJECT_NAME' || updated.key === 'PROJECT_TAGLINE' || updated.key === 'LDAP_ENABLED' || updated.key === 'LDAP_USE_LDAPS') {
|
||||
let needsPublicRefresh = false;
|
||||
for (const setting of visibleSettings) {
|
||||
const updated = await apiFetch<SystemSetting>(
|
||||
'/admin/settings',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(buildSystemSettingPayload(setting))
|
||||
},
|
||||
token
|
||||
);
|
||||
setSettings((current) => sortSettingsByCatalog(current.map((item) => (item.key === updated.key ? updated : item))));
|
||||
if (PUBLIC_SETTINGS_REFRESH_KEYS.has(updated.key)) {
|
||||
needsPublicRefresh = true;
|
||||
}
|
||||
}
|
||||
if (needsPublicRefresh) {
|
||||
await refreshPublicSettings();
|
||||
}
|
||||
showToast('Настройка сохранена');
|
||||
showToast(`Блок «${GROUP_LABELS[groupKey] ?? groupKey}» сохранён`);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : 'Не удалось сохранить настройку');
|
||||
showToast(error instanceof Error ? error.message : 'Не удалось сохранить настройки блока');
|
||||
} finally {
|
||||
setSavingKey(null);
|
||||
setSavingGroup(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +113,14 @@ export default function AdminSettingsPage() {
|
||||
async function toggleProvider(provider: SocialProvider) {
|
||||
if (!token) return;
|
||||
try {
|
||||
const updated = await apiFetch<SocialProvider>('/admin/settings/oauth/providers', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...toProviderPayload(provider), isEnabled: !provider.isEnabled })
|
||||
}, token);
|
||||
const updated = await apiFetch<SocialProvider>(
|
||||
'/admin/settings/oauth/providers',
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...toProviderPayload(provider), isEnabled: !provider.isEnabled })
|
||||
},
|
||||
token
|
||||
);
|
||||
setProviders((current) => current.map((item) => (item.providerName === updated.providerName ? updated : item)));
|
||||
showToast(updated.isEnabled ? 'Провайдер включён' : 'Провайдер отключён');
|
||||
} catch (error) {
|
||||
@@ -102,6 +128,22 @@ export default function AdminSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOAuthProvider(provider: SocialProvider) {
|
||||
if (!token) return;
|
||||
setSavingOAuthProvider(provider.providerName);
|
||||
try {
|
||||
await apiFetch('/admin/settings/oauth/providers', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(toProviderPayload(provider))
|
||||
}, token);
|
||||
showToast(`Провайдер ${provider.providerName} сохранён`);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : 'Ошибка сохранения провайдера');
|
||||
} finally {
|
||||
setSavingOAuthProvider(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!user?.canManageSettings) {
|
||||
return (
|
||||
<AdminShell active="/admin/settings">
|
||||
@@ -114,7 +156,9 @@ export default function AdminSettingsPage() {
|
||||
<AdminShell active="/admin/settings">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-medium">Глобальные настройки</h2>
|
||||
<p className="mt-2 text-[#667085]">Параметры хранятся в SystemSetting и применяются сервисами без релиза frontend.</p>
|
||||
<p className="mt-2 text-[#667085]">
|
||||
Параметры сгруппированы по блокам — измените нужные поля и сохраните весь блок одной кнопкой.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -124,112 +168,101 @@ export default function AdminSettingsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{[...groupedSettings.entries()].map(([groupKey, groupSettings]) => (
|
||||
<section key={groupKey} className="mb-10">
|
||||
<h3 className="mb-4 text-xl font-medium">{SYSTEM_SETTING_GROUPS[groupKey] ?? 'Прочие настройки'}</h3>
|
||||
<div className="space-y-3">
|
||||
{groupSettings.filter((setting) => isSettingVisible(setting, settings)).map((setting) => {
|
||||
const meta = getSettingMeta(setting.key);
|
||||
const label = meta?.label ?? setting.key;
|
||||
const type = meta?.type ?? 'text';
|
||||
{[...groupedSettings.entries()].map(([groupKey, groupSettings]) => {
|
||||
const visibleSettings = groupSettings.filter((setting) => isSettingVisible(setting, settings));
|
||||
const saving = savingGroup === groupKey;
|
||||
|
||||
return (
|
||||
<div key={setting.key} className="rounded-[24px] bg-[#f4f5f8] p-5">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Settings2 className="h-4 w-4 shrink-0" />
|
||||
{label}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-[#667085]">{meta?.hint ?? setting.description ?? setting.key}</p>
|
||||
</div>
|
||||
return (
|
||||
<section key={groupKey} className="mb-10">
|
||||
<div className="rounded-[24px] bg-[#f4f5f8] p-5">
|
||||
<div className="mb-4 flex flex-col gap-3 border-b border-[#e0e3ea] pb-4 md:flex-row md:items-center md:justify-between">
|
||||
<h3 className="text-xl font-medium">{SYSTEM_SETTING_GROUPS[groupKey] ?? 'Прочие настройки'}</h3>
|
||||
<Button
|
||||
disabled={saving || visibleSettings.length === 0}
|
||||
onClick={() => void saveSettingsGroup(groupKey, groupSettings)}
|
||||
>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
Сохранить блок
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{type === 'boolean' ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
onClick={() => {
|
||||
updateSettingValue(setting.key, parseBoolean(setting.value) ? 'false' : 'true');
|
||||
}}
|
||||
>
|
||||
{parseBoolean(setting.value) ? <ToggleRight className="h-9 w-9 text-green-600" /> : <ToggleLeft className="h-9 w-9 text-[#a8adbc]" />}
|
||||
</button>
|
||||
) : type === 'select' ? (
|
||||
<select
|
||||
className="h-10 rounded-xl border border-[#e4e7ec] bg-white px-3 text-sm outline-none focus:border-[#98a2b3]"
|
||||
value={setting.value}
|
||||
onChange={(event) => updateSettingValue(setting.key, event.target.value)}
|
||||
>
|
||||
{(meta?.options ?? []).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type={setting.isSecret ? 'password' : type === 'number' ? 'number' : 'text'}
|
||||
value={setting.value}
|
||||
className={setting.isSecret ? 'w-[220px] bg-white' : 'w-[180px] bg-white'}
|
||||
onChange={(event) => updateSettingValue(setting.key, event.target.value)}
|
||||
/>
|
||||
{meta?.unit ? <span className="text-sm text-[#667085]">{meta.unit}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button disabled={savingKey === setting.key} onClick={() => void saveSetting(setting)}>
|
||||
{savingKey === setting.key ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
Сохранить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
<div>
|
||||
{visibleSettings.map((setting) => (
|
||||
<SettingsFieldRow
|
||||
key={setting.key}
|
||||
setting={setting}
|
||||
onChange={(value) => updateSettingValue(setting.key, value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
|
||||
<MessagingSettingsSection
|
||||
settings={settings}
|
||||
token={token}
|
||||
savingKey={savingKey}
|
||||
savingGroup={savingGroup}
|
||||
onUpdateValue={updateSettingValue}
|
||||
onSave={saveSetting}
|
||||
onSaveGroup={saveSettingsGroup}
|
||||
showToast={showToast}
|
||||
/>
|
||||
|
||||
<section className="mt-10">
|
||||
<h3 className="text-xl font-medium">OAuth Social Providers</h3>
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{(providers.length ? providers : [{ id: 'google', providerName: 'google', clientId: '', isEnabled: false }, { id: 'yandex', providerName: 'yandex', clientId: '', isEnabled: false }]).map((provider) => (
|
||||
<h3 className="mb-4 text-xl font-medium">OAuth Social Providers</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{(providers.length
|
||||
? providers
|
||||
: [
|
||||
{ id: 'google', providerName: 'google', clientId: '', isEnabled: false },
|
||||
{ id: 'yandex', providerName: 'yandex', clientId: '', isEnabled: false }
|
||||
]
|
||||
).map((provider) => (
|
||||
<div key={provider.providerName} className="rounded-[24px] bg-[#f4f5f8] p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
{provider.providerName === 'google' ? <Chrome className="h-7 w-7" /> : <div className="flex h-8 w-8 items-center justify-center rounded-full bg-red-500 font-bold text-white">Я</div>}
|
||||
{provider.providerName === 'google' ? (
|
||||
<Chrome className="h-7 w-7" />
|
||||
) : (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-red-500 font-bold text-white">
|
||||
Я
|
||||
</div>
|
||||
)}
|
||||
<button type="button" aria-label="Переключить провайдера" onClick={() => void toggleProvider(provider)}>
|
||||
{provider.isEnabled ? <ToggleRight className="h-8 w-8 text-green-600" /> : <ToggleLeft className="h-8 w-8 text-[#a8adbc]" />}
|
||||
{provider.isEnabled ? (
|
||||
<ToggleRight className="h-8 w-8 text-green-600" />
|
||||
) : (
|
||||
<ToggleLeft className="h-8 w-8 text-[#a8adbc]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<h4 className="mt-5 font-semibold capitalize">{provider.providerName}</h4>
|
||||
<p className="mt-1 text-sm text-[#667085]">{provider.isEnabled ? 'Провайдер включен глобально.' : 'Провайдер отключен.'}</p>
|
||||
<p className="mt-1 text-sm text-[#667085]">
|
||||
{provider.isEnabled ? 'Провайдер включен глобально.' : 'Провайдер отключен.'}
|
||||
</p>
|
||||
<Input
|
||||
className="mt-4 bg-white"
|
||||
value={provider.clientId}
|
||||
placeholder="Client ID"
|
||||
onChange={(event) => setProviders((current) => current.map((item) => (item.providerName === provider.providerName ? { ...item, clientId: event.target.value } : item)))}
|
||||
onChange={(event) =>
|
||||
setProviders((current) =>
|
||||
current.map((item) =>
|
||||
item.providerName === provider.providerName ? { ...item, clientId: event.target.value } : item
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className="mt-3"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
void apiFetch('/admin/settings/oauth/providers', { method: 'PUT', body: JSON.stringify(toProviderPayload(provider)) }, token)
|
||||
.then(() => showToast('Провайдер сохранён'))
|
||||
.catch((error) => showToast(error instanceof Error ? error.message : 'Ошибка сохранения'))
|
||||
}
|
||||
disabled={savingOAuthProvider === provider.providerName}
|
||||
onClick={() => void saveOAuthProvider(provider)}
|
||||
>
|
||||
Сохранить провайдера
|
||||
{savingOAuthProvider === provider.providerName ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
Сохранить блок
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user