173 lines
12 KiB
TypeScript
173 lines
12 KiB
TypeScript
export type SystemSettingFieldType = 'text' | 'number' | 'boolean' | 'select' | 'textarea';
|
||
|
||
export interface SystemSettingMeta {
|
||
key: string;
|
||
label: string;
|
||
group: string;
|
||
type: SystemSettingFieldType;
|
||
unit?: string;
|
||
hint?: string;
|
||
options?: Array<{ value: string; label: string }>;
|
||
showWhen?: { key: string; equals: string };
|
||
}
|
||
|
||
export const SYSTEM_SETTING_GROUPS: Record<string, string> = {
|
||
general: 'Проект и брендинг',
|
||
pin: 'PIN-код и блокировка сессии',
|
||
auth: 'Аутентификация и регистрация',
|
||
ldap: 'LDAP / Active Directory',
|
||
messaging: 'Email и SMS',
|
||
limits: 'Лимиты и ограничения',
|
||
media: 'Медиа и файлы'
|
||
};
|
||
|
||
export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [
|
||
{ key: 'PROJECT_NAME', label: 'Название проекта', group: 'general', type: 'text', hint: 'Отображается в меню, заголовке и боковой панели' },
|
||
{ key: 'PROJECT_TAGLINE', label: 'Слоган проекта', group: 'general', type: 'text' },
|
||
{ key: 'PROJECT_DOMAIN', label: 'Домен IdP', group: 'general', type: 'text', hint: 'Домен без пути, например sso.example.ru' },
|
||
{
|
||
key: 'PUBLIC_API_URL',
|
||
label: 'URL API (OAuth issuer)',
|
||
group: 'general',
|
||
type: 'text',
|
||
hint: 'Базовый URL API для OAuth/OIDC. Пример: https://sso.example.ru/idp-api или https://api.example.ru'
|
||
},
|
||
{ key: 'PIN_LOCK_TIMEOUT_MINUTES', label: 'Таймаут блокировки PIN', group: 'pin', type: 'number', unit: 'мин' },
|
||
{ key: 'PIN_DELETE_GRACE_MINUTES', label: 'Задержка удаления PIN', group: 'pin', type: 'number', unit: 'мин', hint: 'Сколько ждать после запроса удаления PIN-кода' },
|
||
{ key: 'PIN_REQUIRE_ON_DELETE', label: 'Требовать PIN при удалении', group: 'pin', type: 'boolean' },
|
||
{ key: 'PIN_MIN_LENGTH', label: 'Мин. длина PIN', group: 'pin', type: 'number', unit: 'цифр' },
|
||
{ key: 'PIN_MAX_LENGTH', label: 'Макс. длина PIN', group: 'pin', type: 'number', unit: 'цифр' },
|
||
{ key: 'OTP_EXPIRY_MINUTES', label: 'Срок жизни OTP', group: 'auth', type: 'number', unit: 'мин' },
|
||
{ key: 'OTP_RESEND_COOLDOWN_SECONDS', label: 'Пауза перед повторной отправкой OTP', group: 'auth', type: 'number', unit: 'сек', hint: 'Минимальный интервал между запросами нового кода (обычно 60)' },
|
||
{ key: 'OTP_MAX_ATTEMPTS', label: 'Попыток ввода OTP', group: 'auth', type: 'number' },
|
||
{
|
||
key: 'ACCOUNT_DELETE_GRACE_DAYS',
|
||
label: 'Отложенное удаление аккаунта',
|
||
group: 'auth',
|
||
type: 'number',
|
||
unit: 'дн',
|
||
hint: 'Срок ожидания перед окончательным удалением профиля после запроса пользователя'
|
||
},
|
||
{ key: 'SESSION_REFRESH_DAYS', label: 'Срок refresh-токена', group: 'auth', type: 'number', unit: 'дн' },
|
||
{ key: 'PASSWORD_MIN_LENGTH', label: 'Мин. длина пароля', group: 'auth', type: 'number', unit: 'симв' },
|
||
{ key: 'REGISTRATION_ENABLED', label: 'Регистрация включена', group: 'auth', type: 'boolean' },
|
||
{ key: 'MAINTENANCE_MODE', label: 'Режим обслуживания', group: 'auth', type: 'boolean', hint: 'Блокирует вход для обычных пользователей' },
|
||
{ key: 'LDAP_ENABLED', label: 'LDAP включён', group: 'ldap', type: 'boolean', hint: 'Показывает кнопку входа через LDAP на странице авторизации' },
|
||
{ key: 'LDAP_USE_LDAPS', label: 'Использовать LDAPS', group: 'ldap', type: 'boolean', hint: 'Шифрованное подключение (порт 636)' },
|
||
{ key: 'LDAP_HOST', label: 'Хост LDAP', group: 'ldap', type: 'text', hint: 'DC-1.domain.local или IP контроллера (192.168.x.x), если Docker не резолвит AD-имена' },
|
||
{ key: 'LDAP_PORT', label: 'Порт LDAP', group: 'ldap', type: 'number', hint: '636 для LDAPS, 389 для LDAP. Не используйте 640.' },
|
||
{ key: 'LDAP_DOMAIN', label: 'Домен AD', group: 'ldap', type: 'text', hint: 'mvkug.local — для bind вида user@domain' },
|
||
{ key: 'LDAP_SKIP_TLS_VERIFY', label: 'Не проверять TLS', group: 'ldap', type: 'boolean', hint: 'Только для тестовых окружений' },
|
||
{ key: 'LDAP_BASE_DN', label: 'Base DN (поиск пользователей)', group: 'ldap', type: 'text', hint: 'OU или CN контейнера с пользователями. Напр. OU=Lugansk,DC=mvkug,DC=local. Если CN не работает — попробуйте OU' },
|
||
{ key: 'LDAP_BIND_USERNAME', label: 'Логин сервисной УЗ', group: 'ldap', type: 'text', hint: 'mvkadmin или mvkug\\mvkadmin — учётная запись для подключения к AD' },
|
||
{ key: 'LDAP_BIND_DN', label: 'Bind DN (опционально)', group: 'ldap', type: 'text', hint: 'Полный DN сервисной УЗ. Оставьте пустым, если указан логин выше' },
|
||
{ key: 'LDAP_BIND_PASSWORD', label: 'Пароль сервисной УЗ', group: 'ldap', type: 'text', hint: 'Секретное значение' },
|
||
{ key: 'LDAP_USER_FILTER', label: 'Фильтр пользователя', group: 'ldap', type: 'text', hint: 'Placeholder {username}' },
|
||
{ key: 'LDAP_USERNAME_ATTR', label: 'Атрибут логина', group: 'ldap', type: 'text' },
|
||
{ key: 'LDAP_EMAIL_ATTR', label: 'Атрибут почты', group: 'ldap', type: 'text' },
|
||
{ key: 'LDAP_DISPLAY_NAME_ATTR', label: 'Атрибут имени', group: 'ldap', type: 'text' },
|
||
{ key: 'EMAIL_ENABLED', label: 'Email включён', group: 'messaging', type: 'boolean', hint: 'Отправка OTP-кодов на почту через SMTP' },
|
||
{
|
||
key: 'EMAIL_PROVIDER',
|
||
label: 'Email-провайдер',
|
||
group: 'messaging',
|
||
type: 'select',
|
||
options: [
|
||
{ value: 'smtp', label: 'SMTP (Yandex, Gmail, Mail.ru и др.)' },
|
||
{ value: 'console', label: 'Только лог (отладка)' }
|
||
],
|
||
showWhen: { key: 'EMAIL_ENABLED', equals: 'true' }
|
||
},
|
||
{ key: 'EMAIL_SMTP_HOST', label: 'SMTP-хост', group: 'messaging', type: 'text', hint: 'smtp.yandex.ru, smtp.gmail.com', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_SMTP_PORT', label: 'SMTP-порт', group: 'messaging', type: 'number', unit: 'порт', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_SMTP_SECURE', label: 'SMTP SSL/TLS', group: 'messaging', type: 'boolean', hint: 'Включите для порта 465', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_SMTP_USER', label: 'SMTP-логин', group: 'messaging', type: 'text', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_SMTP_PASSWORD', label: 'SMTP-пароль', group: 'messaging', type: 'text', hint: 'App-password для Gmail/Yandex', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_FROM_ADDRESS', label: 'Email отправителя', group: 'messaging', type: 'text', hint: 'noreply@yourdomain.ru', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'EMAIL_FROM_NAME', label: 'Имя отправителя', group: 'messaging', type: 'text', showWhen: { key: 'EMAIL_ENABLED', equals: 'true' } },
|
||
{ key: 'SMS_ENABLED', label: 'SMS включён', group: 'messaging', type: 'boolean', hint: 'Отправка OTP-кодов по SMS' },
|
||
{
|
||
key: 'SMS_PROVIDER',
|
||
label: 'SMS-провайдер',
|
||
group: 'messaging',
|
||
type: 'select',
|
||
options: [
|
||
{ value: 'smsru', label: 'sms.ru' },
|
||
{ value: 'smsc', label: 'smsc.ru' },
|
||
{ value: 'huawei_modem', label: 'Huawei E5573 (локальный шлюз)' },
|
||
{ value: 'console', label: 'Только лог (отладка)' }
|
||
],
|
||
showWhen: { key: 'SMS_ENABLED', equals: 'true' }
|
||
},
|
||
{
|
||
key: 'SMS_GATEWAY_URL',
|
||
label: 'URL локального SMS-шлюза',
|
||
group: 'messaging',
|
||
type: 'text',
|
||
hint: 'IP или URL модема Huawei HiLink, например http://192.168.8.1',
|
||
showWhen: { key: 'SMS_ENABLED', equals: 'true' }
|
||
},
|
||
{
|
||
key: 'SMS_GATEWAY_USERNAME',
|
||
label: 'Логин модема Huawei',
|
||
group: 'messaging',
|
||
type: 'text',
|
||
hint: 'Обычно admin',
|
||
showWhen: { key: 'SMS_ENABLED', equals: 'true' }
|
||
},
|
||
{
|
||
key: 'SMS_GATEWAY_PASSWORD',
|
||
label: 'Пароль модема Huawei',
|
||
group: 'messaging',
|
||
type: 'text',
|
||
hint: 'Пароль веб-интерфейса модема (требуется для отправки SMS)',
|
||
showWhen: { key: 'SMS_ENABLED', equals: 'true' }
|
||
},
|
||
{
|
||
key: 'SMS_GATEWAY_MESSAGE_TEMPLATE',
|
||
label: 'Шаблон текста SMS (Huawei)',
|
||
group: 'messaging',
|
||
type: 'textarea',
|
||
hint: 'Теги: {{appname}} — название проекта, {{code}} — OTP-код, {{expiry}} — срок действия в минутах',
|
||
showWhen: { key: 'SMS_ENABLED', equals: 'true' }
|
||
},
|
||
{ key: 'SMS_API_KEY', label: 'API-ключ sms.ru', group: 'messaging', type: 'text', hint: 'api_id из личного кабинета sms.ru', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||
{ key: 'SMS_LOGIN', label: 'Логин smsc.ru', group: 'messaging', type: 'text', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||
{ key: 'SMS_PASSWORD', label: 'Пароль smsc.ru', group: 'messaging', type: 'text', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||
{ key: 'SMS_SENDER', label: 'Имя отправителя SMS', group: 'messaging', type: 'text', hint: 'Опционально, если одобрено провайдером', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||
{ key: 'MAX_FAMILY_MEMBERS', label: 'Макс. участников семьи', group: 'limits', type: 'number' },
|
||
{ key: 'MAX_ACTIVE_SESSIONS', label: 'Макс. активных сессий', group: 'limits', type: 'number' },
|
||
{ key: 'AVATAR_URL_TTL_MINUTES', label: 'TTL ссылки на аватар', group: 'media', type: 'number', unit: 'мин' }
|
||
];
|
||
|
||
export function getSettingMeta(key: string) {
|
||
return SYSTEM_SETTING_CATALOG.find((item) => item.key === key);
|
||
}
|
||
|
||
export function isSettingVisible(setting: { key: string; value: string }, allSettings: Array<{ key: string; value: string }>) {
|
||
const meta = getSettingMeta(setting.key);
|
||
if (!meta?.showWhen) return true;
|
||
const parent = allSettings.find((item) => item.key === meta.showWhen!.key);
|
||
const parentValue = parent?.value ?? 'false';
|
||
return parseBooleanValue(parentValue) === parseBooleanValue(meta.showWhen.equals);
|
||
}
|
||
|
||
function parseBooleanValue(value: string) {
|
||
return ['true', '1', 'yes'].includes(value.trim().toLowerCase());
|
||
}
|
||
|
||
export const MESSAGING_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) => item.group === 'messaging').map((item) => item.key);
|
||
|
||
export const EMAIL_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) =>
|
||
item.key.startsWith('EMAIL_')
|
||
).map((item) => item.key);
|
||
|
||
export const SMS_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) =>
|
||
item.key.startsWith('SMS_')
|
||
).map((item) => item.key);
|
||
|
||
export function sortSettingsByCatalog<T extends { key: string }>(settings: T[]) {
|
||
const order = new Map(SYSTEM_SETTING_CATALOG.map((item, index) => [item.key, index]));
|
||
return [...settings].sort((a, b) => (order.get(a.key) ?? 999) - (order.get(b.key) ?? 999));
|
||
}
|