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 = { general: 'Проект и брендинг', onetap: 'One Tap Login / FedCM', pin: 'PIN-код и блокировка сессии', auth: 'Аутентификация и регистрация', ldap: 'LDAP / Active Directory', messaging: 'Email и SMS', firebase: 'Firebase Push', 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 (SSO)', group: 'general', type: 'text', hint: 'Frontend-домен без пути, например sso.idpmvk.lpr' }, { key: 'PUBLIC_API_URL', label: 'URL API (OAuth issuer)', group: 'general', type: 'text', hint: 'Публичный URL, доступный из браузера. Пример: https://api.idpmvk.lpr (отдельный домен API) или https://sso.idpmvk.lpr/idp-api (same-origin)' }, { key: 'PUBLIC_FRONTEND_URL', label: 'URL Frontend IdP', group: 'general', type: 'text', hint: 'Публичный адрес UI IdP. Пример: https://sso.idpmvk.lpr' }, { key: 'ONE_TAP_ENABLED', label: 'One Tap Login включён', group: 'onetap', type: 'boolean', hint: 'FedCM и виджет sso-widget.js. При отключении остаётся только стандартный OAuth redirect' }, { key: 'FEDCM_PRIVACY_POLICY_URL', label: 'FedCM — политика конфиденциальности', group: 'onetap', type: 'text', hint: 'URL для UI FedCM. Пусто — используется страница «Данные» IdP', showWhen: { key: 'ONE_TAP_ENABLED', equals: 'true' } }, { key: 'FEDCM_TERMS_URL', label: 'FedCM — условия использования', group: 'onetap', type: 'text', hint: 'URL для UI FedCM. Пусто — используется страница «Данные» IdP', showWhen: { key: 'ONE_TAP_ENABLED', equals: 'true' } }, { 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: 'FIREBASE_PUSH_ENABLED', label: 'Push-уведомления включены', group: 'firebase', type: 'boolean', hint: 'Отправка push через Firebase Cloud Messaging на зарегистрированные устройства' }, { key: 'FIREBASE_PROJECT_ID', label: 'Firebase Project ID', group: 'firebase', type: 'text', hint: 'project_id из консоли Firebase и JSON сервисного аккаунта', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_CLIENT_EMAIL', label: 'Service Account Email', group: 'firebase', type: 'text', hint: 'client_email из JSON ключа сервисного аккаунта', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_PRIVATE_KEY', label: 'Service Account Private Key', group: 'firebase', type: 'textarea', hint: 'private_key из JSON. Вставьте как есть — переносы строк сохранятся', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_WEB_API_KEY', label: 'Web API Key', group: 'firebase', type: 'text', hint: 'Публичный ключ для клиентского Firebase SDK (веб и PWA)', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_WEB_APP_ID', label: 'Web App ID', group: 'firebase', type: 'text', hint: 'appId веб-приложения из консоли Firebase', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_WEB_MESSAGING_SENDER_ID', label: 'Messaging Sender ID', group: 'firebase', type: 'text', hint: 'senderId для FCM в браузере', showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' } }, { key: 'FIREBASE_WEB_VAPID_KEY', label: 'Web Push VAPID Key', group: 'firebase', type: 'text', hint: 'Пара ключей Web Push certificates → Key pair из Firebase Cloud Messaging', showWhen: { key: 'FIREBASE_PUSH_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 FIREBASE_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) => item.group === 'firebase').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(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)); }