feat: notifications, audit logging, and admin improvements
- Add centralized notification service with webhook (Discord/Slack) and email (SMTP) support, triggered for trip invites, booking changes, collab messages, and trip reminders - Webhook sends one message per event (group channel); email sends individually per trip member, excluding the actor - Discord invite notifications now include the invited user's name - Add LOG_LEVEL env var (info/debug) controlling console and file output - INFO logs show user email, action, and IP for audit events; errors for HTTP requests - DEBUG logs show every request with full body/query (passwords redacted), audit details, notification params, and webhook payloads - Add persistent trek.log file logging with 10MB rotation (5 files) in /app/data/logs/ - Color-coded log levels in Docker console output - Timestamps without timezone name (user sets TZ via Docker) - Add Test Webhook and Save buttons to admin notification settings - Move notification event toggles to admin panel - Add daily trip reminder scheduler (9 AM, timezone-aware) - Wire up booking create/update/delete and collab message notifications - Add i18n keys for notification UI across all 13 languages Made-with: Cursor
This commit is contained in:
@@ -316,6 +316,7 @@ export const notificationsApi = {
|
||||
getPreferences: () => apiClient.get('/notifications/preferences').then(r => r.data),
|
||||
updatePreferences: (prefs: Record<string, boolean>) => apiClient.put('/notifications/preferences', prefs).then(r => r.data),
|
||||
testSmtp: (email?: string) => apiClient.post('/notifications/test-smtp', { email }).then(r => r.data),
|
||||
testWebhook: () => apiClient.post('/notifications/test-webhook').then(r => r.data),
|
||||
}
|
||||
|
||||
export default apiClient
|
||||
|
||||
@@ -15,7 +15,11 @@ interface AuditEntry {
|
||||
ip: string | null
|
||||
}
|
||||
|
||||
export default function AuditLogPanel(): React.ReactElement {
|
||||
interface AuditLogPanelProps {
|
||||
serverTimezone?: string
|
||||
}
|
||||
|
||||
export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): React.ReactElement {
|
||||
const { t, locale } = useTranslation()
|
||||
const [entries, setEntries] = useState<AuditEntry[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
@@ -66,9 +70,10 @@ export default function AuditLogPanel(): React.ReactElement {
|
||||
|
||||
const fmtTime = (iso: string) => {
|
||||
try {
|
||||
return new Date(iso).toLocaleString(locale, {
|
||||
return new Date(iso.endsWith('Z') ? iso : iso + 'Z').toLocaleString(locale, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium',
|
||||
timeZone: serverTimezone || undefined,
|
||||
})
|
||||
} catch {
|
||||
return iso
|
||||
|
||||
@@ -29,6 +29,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'البريد الإلكتروني',
|
||||
'common.password': 'كلمة المرور',
|
||||
'common.saving': 'جارٍ الحفظ...',
|
||||
'common.saved': 'تم الحفظ',
|
||||
'common.update': 'تحديث',
|
||||
'common.change': 'تغيير',
|
||||
'common.uploading': 'جارٍ الرفع...',
|
||||
@@ -154,8 +155,23 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'رسائل الدردشة (Collab)',
|
||||
'settings.notifyPackingTagged': 'قائمة الأمتعة: التعيينات',
|
||||
'settings.notifyWebhook': 'إشعارات Webhook',
|
||||
'settings.notificationsDisabled': 'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
|
||||
'settings.notificationsActive': 'القناة النشطة',
|
||||
'settings.notificationsManagedByAdmin': 'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
|
||||
'admin.notifications.title': 'الإشعارات',
|
||||
'admin.notifications.hint': 'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
|
||||
'admin.notifications.none': 'معطّل',
|
||||
'admin.notifications.email': 'البريد الإلكتروني (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'أحداث الإشعارات',
|
||||
'admin.notifications.eventsHint': 'اختر الأحداث التي تُفعّل الإشعارات لجميع المستخدمين.',
|
||||
'admin.notifications.save': 'حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.saved': 'تم حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.testWebhook': 'إرسال webhook تجريبي',
|
||||
'admin.notifications.testWebhookSuccess': 'تم إرسال webhook التجريبي بنجاح',
|
||||
'admin.notifications.testWebhookFailed': 'فشل إرسال webhook التجريبي',
|
||||
'admin.smtp.title': 'البريد والإشعارات',
|
||||
'admin.smtp.hint': 'تكوين SMTP لإشعارات البريد الإلكتروني. اختياري: عنوان Webhook لـ Discord أو Slack وغيرها.',
|
||||
'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.',
|
||||
'admin.smtp.testButton': 'إرسال بريد تجريبي',
|
||||
'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح',
|
||||
'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي',
|
||||
@@ -301,6 +317,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'دخول',
|
||||
'login.createAdmin': 'إنشاء حساب مسؤول',
|
||||
'login.createAdminHint': 'أعد إعداد أول حساب مسؤول لـ TREK.',
|
||||
'login.setNewPassword': 'تعيين كلمة مرور جديدة',
|
||||
'login.setNewPasswordHint': 'يجب عليك تغيير كلمة المرور قبل المتابعة.',
|
||||
'login.createAccount': 'إنشاء حساب',
|
||||
'login.createAccountHint': 'سجّل حسابًا جديدًا.',
|
||||
'login.creating': 'جارٍ الإنشاء…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Senha',
|
||||
'common.saving': 'Salvando...',
|
||||
'common.saved': 'Salvo',
|
||||
'common.update': 'Atualizar',
|
||||
'common.change': 'Alterar',
|
||||
'common.uploading': 'Enviando…',
|
||||
@@ -149,8 +150,23 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Mensagens de chat (Colab)',
|
||||
'settings.notifyPackingTagged': 'Lista de mala: atribuições',
|
||||
'settings.notifyWebhook': 'Notificações webhook',
|
||||
'settings.notificationsDisabled': 'As notificações não estão configuradas. Peça a um administrador para ativar notificações por e-mail ou webhook.',
|
||||
'settings.notificationsActive': 'Canal ativo',
|
||||
'settings.notificationsManagedByAdmin': 'Os eventos de notificação são configurados pelo administrador.',
|
||||
'admin.notifications.title': 'Notificações',
|
||||
'admin.notifications.hint': 'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
|
||||
'admin.notifications.none': 'Desativado',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificação',
|
||||
'admin.notifications.eventsHint': 'Escolha quais eventos acionam notificações para todos os usuários.',
|
||||
'admin.notifications.save': 'Salvar configurações de notificação',
|
||||
'admin.notifications.saved': 'Configurações de notificação salvas',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de teste',
|
||||
'admin.notifications.testWebhookSuccess': 'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste',
|
||||
'admin.smtp.title': 'E-mail e notificações',
|
||||
'admin.smtp.hint': 'Configuração SMTP para notificações por e-mail. Opcional: URL webhook para Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.',
|
||||
'admin.smtp.testButton': 'Enviar e-mail de teste',
|
||||
'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso',
|
||||
'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste',
|
||||
@@ -296,6 +312,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Entrar',
|
||||
'login.createAdmin': 'Criar conta de administrador',
|
||||
'login.createAdminHint': 'Configure a primeira conta de administrador do TREK.',
|
||||
'login.setNewPassword': 'Definir nova senha',
|
||||
'login.setNewPasswordHint': 'Você deve alterar sua senha antes de continuar.',
|
||||
'login.createAccount': 'Criar conta',
|
||||
'login.createAccountHint': 'Cadastre uma nova conta.',
|
||||
'login.creating': 'Criando…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Heslo',
|
||||
'common.saving': 'Ukládání...',
|
||||
'common.saved': 'Uloženo',
|
||||
'common.update': 'Aktualizovat',
|
||||
'common.change': 'Změnit',
|
||||
'common.uploading': 'Nahrávání…',
|
||||
@@ -150,6 +151,9 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Zprávy v chatu (Collab)',
|
||||
'settings.notifyPackingTagged': 'Seznam balení: přiřazení',
|
||||
'settings.notifyWebhook': 'Webhook oznámení',
|
||||
'settings.notificationsDisabled': 'Oznámení nejsou nakonfigurována. Požádejte správce o aktivaci e-mailových nebo webhookových oznámení.',
|
||||
'settings.notificationsActive': 'Aktivní kanál',
|
||||
'settings.notificationsManagedByAdmin': 'Události oznámení jsou konfigurovány administrátorem.',
|
||||
'settings.on': 'Zapnuto',
|
||||
'settings.off': 'Vypnuto',
|
||||
'settings.mcp.title': 'Konfigurace MCP',
|
||||
@@ -235,8 +239,20 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mfa.toastEnabled': 'Dvoufaktorové ověření bylo zapnuto',
|
||||
'settings.mfa.toastDisabled': 'Dvoufaktorové ověření bylo vypnuto',
|
||||
'settings.mfa.demoBlocked': 'Není k dispozici v demo režimu',
|
||||
'admin.notifications.title': 'Oznámení',
|
||||
'admin.notifications.hint': 'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.',
|
||||
'admin.notifications.none': 'Vypnuto',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Události oznámení',
|
||||
'admin.notifications.eventsHint': 'Vyberte, které události spouštějí oznámení pro všechny uživatele.',
|
||||
'admin.notifications.save': 'Uložit nastavení oznámení',
|
||||
'admin.notifications.saved': 'Nastavení oznámení uloženo',
|
||||
'admin.notifications.testWebhook': 'Odeslat testovací webhook',
|
||||
'admin.notifications.testWebhookSuccess': 'Testovací webhook úspěšně odeslán',
|
||||
'admin.notifications.testWebhookFailed': 'Odeslání testovacího webhooku se nezdařilo',
|
||||
'admin.smtp.title': 'E-mail a oznámení',
|
||||
'admin.smtp.hint': 'Konfigurace SMTP pro e-mailová oznámení. Volitelně: Webhook URL pro Discord, Slack apod.',
|
||||
'admin.smtp.hint': 'Konfigurace SMTP pro odesílání e-mailových oznámení.',
|
||||
'admin.smtp.testButton': 'Odeslat testovací e-mail',
|
||||
'admin.smtp.testSuccess': 'Testovací e-mail byl úspěšně odeslán',
|
||||
'admin.smtp.testFailed': 'Odeslání testovacího e-mailu se nezdařilo',
|
||||
@@ -297,6 +313,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Přihlásit se',
|
||||
'login.createAdmin': 'Vytvořit účet administrátora',
|
||||
'login.createAdminHint': 'Nastavte první administrátorský účet pro TREK.',
|
||||
'login.setNewPassword': 'Nastavit nové heslo',
|
||||
'login.setNewPasswordHint': 'Před pokračováním musíte změnit heslo.',
|
||||
'login.createAccount': 'Vytvořit účet',
|
||||
'login.createAccountHint': 'Zaregistrujte si nový účet.',
|
||||
'login.creating': 'Vytváření…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-Mail',
|
||||
'common.password': 'Passwort',
|
||||
'common.saving': 'Speichern...',
|
||||
'common.saved': 'Gespeichert',
|
||||
'common.update': 'Aktualisieren',
|
||||
'common.change': 'Ändern',
|
||||
'common.uploading': 'Hochladen…',
|
||||
@@ -149,8 +150,23 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Chat-Nachrichten (Collab)',
|
||||
'settings.notifyPackingTagged': 'Packliste: Zuweisungen',
|
||||
'settings.notifyWebhook': 'Webhook-Benachrichtigungen',
|
||||
'settings.notificationsDisabled': 'Benachrichtigungen sind nicht konfiguriert. Bitten Sie einen Administrator, E-Mail- oder Webhook-Benachrichtungen zu aktivieren.',
|
||||
'settings.notificationsActive': 'Aktiver Kanal',
|
||||
'settings.notificationsManagedByAdmin': 'Benachrichtigungsereignisse werden vom Administrator konfiguriert.',
|
||||
'admin.notifications.title': 'Benachrichtigungen',
|
||||
'admin.notifications.hint': 'Wählen Sie einen Benachrichtigungskanal. Es kann nur einer gleichzeitig aktiv sein.',
|
||||
'admin.notifications.none': 'Deaktiviert',
|
||||
'admin.notifications.email': 'E-Mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Benachrichtigungsereignisse',
|
||||
'admin.notifications.eventsHint': 'Wähle, welche Ereignisse Benachrichtigungen für alle Benutzer auslösen.',
|
||||
'admin.notifications.save': 'Benachrichtigungseinstellungen speichern',
|
||||
'admin.notifications.saved': 'Benachrichtigungseinstellungen gespeichert',
|
||||
'admin.notifications.testWebhook': 'Test-Webhook senden',
|
||||
'admin.notifications.testWebhookSuccess': 'Test-Webhook erfolgreich gesendet',
|
||||
'admin.notifications.testWebhookFailed': 'Test-Webhook fehlgeschlagen',
|
||||
'admin.smtp.title': 'E-Mail & Benachrichtigungen',
|
||||
'admin.smtp.hint': 'SMTP-Konfiguration für E-Mail-Benachrichtigungen. Optional: Webhook-URL für Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'SMTP-Konfiguration zum Versenden von E-Mail-Benachrichtigungen.',
|
||||
'admin.smtp.testButton': 'Test-E-Mail senden',
|
||||
'admin.smtp.testSuccess': 'Test-E-Mail erfolgreich gesendet',
|
||||
'admin.smtp.testFailed': 'Test-E-Mail fehlgeschlagen',
|
||||
@@ -296,6 +312,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Anmelden',
|
||||
'login.createAdmin': 'Admin-Konto erstellen',
|
||||
'login.createAdminHint': 'Erstelle das erste Admin-Konto für TREK.',
|
||||
'login.setNewPassword': 'Neues Passwort festlegen',
|
||||
'login.setNewPasswordHint': 'Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können.',
|
||||
'login.createAccount': 'Konto erstellen',
|
||||
'login.createAccountHint': 'Neues Konto registrieren.',
|
||||
'login.creating': 'Erstelle…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Password',
|
||||
'common.saving': 'Saving...',
|
||||
'common.saved': 'Saved',
|
||||
'common.update': 'Update',
|
||||
'common.change': 'Change',
|
||||
'common.uploading': 'Uploading…',
|
||||
@@ -149,11 +150,26 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Chat messages (Collab)',
|
||||
'settings.notifyPackingTagged': 'Packing list: assignments',
|
||||
'settings.notifyWebhook': 'Webhook notifications',
|
||||
'admin.notifications.title': 'Notifications',
|
||||
'admin.notifications.hint': 'Choose one notification channel. Only one can be active at a time.',
|
||||
'admin.notifications.none': 'Disabled',
|
||||
'admin.notifications.email': 'Email (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Notification Events',
|
||||
'admin.notifications.eventsHint': 'Choose which events trigger notifications for all users.',
|
||||
'admin.notifications.save': 'Save notification settings',
|
||||
'admin.notifications.saved': 'Notification settings saved',
|
||||
'admin.notifications.testWebhook': 'Send test webhook',
|
||||
'admin.notifications.testWebhookSuccess': 'Test webhook sent successfully',
|
||||
'admin.notifications.testWebhookFailed': 'Test webhook failed',
|
||||
'admin.smtp.title': 'Email & Notifications',
|
||||
'admin.smtp.hint': 'SMTP configuration for email notifications. Optional: Webhook URL for Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'SMTP configuration for sending email notifications.',
|
||||
'admin.smtp.testButton': 'Send test email',
|
||||
'admin.smtp.testSuccess': 'Test email sent successfully',
|
||||
'admin.smtp.testFailed': 'Test email failed',
|
||||
'settings.notificationsDisabled': 'Notifications are not configured. Ask an admin to enable email or webhook notifications.',
|
||||
'settings.notificationsActive': 'Active channel',
|
||||
'settings.notificationsManagedByAdmin': 'Notification events are configured by your administrator.',
|
||||
'dayplan.icsTooltip': 'Export calendar (ICS)',
|
||||
'share.linkTitle': 'Public Link',
|
||||
'share.linkHint': 'Create a link anyone can use to view this trip without logging in. Read-only — no editing possible.',
|
||||
@@ -227,6 +243,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.passwordMismatch': 'Passwords do not match',
|
||||
'settings.passwordWeak': 'Password must contain uppercase, lowercase, and a number',
|
||||
'settings.passwordChanged': 'Password changed successfully',
|
||||
'settings.mustChangePassword': 'You must change your password before you can continue. Please set a new password below.',
|
||||
'settings.deleteAccount': 'Delete account',
|
||||
'settings.deleteAccountTitle': 'Delete your account?',
|
||||
'settings.deleteAccountWarning': 'Your account and all your trips, places, and files will be permanently deleted. This action cannot be undone.',
|
||||
@@ -296,6 +313,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Sign In',
|
||||
'login.createAdmin': 'Create Admin Account',
|
||||
'login.createAdminHint': 'Set up the first admin account for TREK.',
|
||||
'login.setNewPassword': 'Set New Password',
|
||||
'login.setNewPasswordHint': 'You must change your password before continuing.',
|
||||
'login.createAccount': 'Create Account',
|
||||
'login.createAccountHint': 'Register a new account.',
|
||||
'login.creating': 'Creating…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const es: Record<string, string> = {
|
||||
'common.email': 'Correo',
|
||||
'common.password': 'Contraseña',
|
||||
'common.saving': 'Guardando...',
|
||||
'common.saved': 'Guardado',
|
||||
'common.update': 'Actualizar',
|
||||
'common.change': 'Cambiar',
|
||||
'common.uploading': 'Subiendo…',
|
||||
@@ -150,8 +151,23 @@ const es: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': 'Mensajes de chat (Collab)',
|
||||
'settings.notifyPackingTagged': 'Lista de equipaje: asignaciones',
|
||||
'settings.notifyWebhook': 'Notificaciones webhook',
|
||||
'settings.notificationsDisabled': 'Las notificaciones no están configuradas. Pida a un administrador que active las notificaciones por correo o webhook.',
|
||||
'settings.notificationsActive': 'Canal activo',
|
||||
'settings.notificationsManagedByAdmin': 'Los eventos de notificación son configurados por el administrador.',
|
||||
'admin.notifications.title': 'Notificaciones',
|
||||
'admin.notifications.hint': 'Elija un canal de notificación. Solo uno puede estar activo a la vez.',
|
||||
'admin.notifications.none': 'Desactivado',
|
||||
'admin.notifications.email': 'Correo (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventos de notificación',
|
||||
'admin.notifications.eventsHint': 'Elige qué eventos activan notificaciones para todos los usuarios.',
|
||||
'admin.notifications.save': 'Guardar configuración de notificaciones',
|
||||
'admin.notifications.saved': 'Configuración de notificaciones guardada',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de prueba',
|
||||
'admin.notifications.testWebhookSuccess': 'Webhook de prueba enviado correctamente',
|
||||
'admin.notifications.testWebhookFailed': 'Error al enviar webhook de prueba',
|
||||
'admin.smtp.title': 'Correo y notificaciones',
|
||||
'admin.smtp.hint': 'Configuración SMTP para notificaciones por correo. Opcional: URL webhook para Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'Configuración SMTP para el envío de notificaciones por correo.',
|
||||
'admin.smtp.testButton': 'Enviar correo de prueba',
|
||||
'admin.smtp.testSuccess': 'Correo de prueba enviado correctamente',
|
||||
'admin.smtp.testFailed': 'Error al enviar correo de prueba',
|
||||
@@ -295,6 +311,8 @@ const es: Record<string, string> = {
|
||||
'login.signIn': 'Entrar',
|
||||
'login.createAdmin': 'Crear cuenta de administrador',
|
||||
'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.',
|
||||
'login.setNewPassword': 'Establecer nueva contraseña',
|
||||
'login.setNewPasswordHint': 'Debe cambiar su contraseña antes de continuar.',
|
||||
'login.createAccount': 'Crear cuenta',
|
||||
'login.createAccountHint': 'Crea una cuenta nueva.',
|
||||
'login.creating': 'Creando…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const fr: Record<string, string> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Mot de passe',
|
||||
'common.saving': 'Enregistrement…',
|
||||
'common.saved': 'Enregistré',
|
||||
'common.update': 'Mettre à jour',
|
||||
'common.change': 'Modifier',
|
||||
'common.uploading': 'Import en cours…',
|
||||
@@ -149,8 +150,23 @@ const fr: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': 'Messages de chat (Collab)',
|
||||
'settings.notifyPackingTagged': 'Liste de bagages : attributions',
|
||||
'settings.notifyWebhook': 'Notifications webhook',
|
||||
'settings.notificationsDisabled': 'Les notifications ne sont pas configurées. Demandez à un administrateur d\'activer les notifications par e-mail ou webhook.',
|
||||
'settings.notificationsActive': 'Canal actif',
|
||||
'settings.notificationsManagedByAdmin': 'Les événements de notification sont configurés par votre administrateur.',
|
||||
'admin.notifications.title': 'Notifications',
|
||||
'admin.notifications.hint': 'Choisissez un canal de notification. Un seul peut être actif à la fois.',
|
||||
'admin.notifications.none': 'Désactivé',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Événements de notification',
|
||||
'admin.notifications.eventsHint': 'Choisissez quels événements déclenchent des notifications pour tous les utilisateurs.',
|
||||
'admin.notifications.save': 'Enregistrer les paramètres de notification',
|
||||
'admin.notifications.saved': 'Paramètres de notification enregistrés',
|
||||
'admin.notifications.testWebhook': 'Envoyer un webhook de test',
|
||||
'admin.notifications.testWebhookSuccess': 'Webhook de test envoyé avec succès',
|
||||
'admin.notifications.testWebhookFailed': 'Échec du webhook de test',
|
||||
'admin.smtp.title': 'E-mail et notifications',
|
||||
'admin.smtp.hint': 'Configuration SMTP pour les notifications par e-mail. Optionnel : URL webhook pour Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'Configuration SMTP pour l\'envoi des notifications par e-mail.',
|
||||
'admin.smtp.testButton': 'Envoyer un e-mail de test',
|
||||
'admin.smtp.testSuccess': 'E-mail de test envoyé avec succès',
|
||||
'admin.smtp.testFailed': 'Échec de l\'e-mail de test',
|
||||
@@ -296,6 +312,8 @@ const fr: Record<string, string> = {
|
||||
'login.signIn': 'Se connecter',
|
||||
'login.createAdmin': 'Créer un compte administrateur',
|
||||
'login.createAdminHint': 'Configurez le premier compte administrateur pour TREK.',
|
||||
'login.setNewPassword': 'Définir un nouveau mot de passe',
|
||||
'login.setNewPasswordHint': 'Vous devez changer votre mot de passe avant de continuer.',
|
||||
'login.createAccount': 'Créer un compte',
|
||||
'login.createAccountHint': 'Créez un nouveau compte.',
|
||||
'login.creating': 'Création…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Jelszó',
|
||||
'common.saving': 'Mentés...',
|
||||
'common.saved': 'Mentve',
|
||||
'common.update': 'Frissítés',
|
||||
'common.change': 'Módosítás',
|
||||
'common.uploading': 'Feltöltés…',
|
||||
@@ -149,6 +150,9 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Csevegés üzenetek (Collab)',
|
||||
'settings.notifyPackingTagged': 'Csomagolási lista: hozzárendelések',
|
||||
'settings.notifyWebhook': 'Webhook értesítések',
|
||||
'settings.notificationsDisabled': 'Az értesítések nincsenek beállítva. Kérje meg a rendszergazdát, hogy engedélyezze az e-mail vagy webhook értesítéseket.',
|
||||
'settings.notificationsActive': 'Aktív csatorna',
|
||||
'settings.notificationsManagedByAdmin': 'Az értesítési eseményeket az adminisztrátor konfigurálja.',
|
||||
'settings.on': 'Be',
|
||||
'settings.off': 'Ki',
|
||||
'settings.mcp.title': 'MCP konfiguráció',
|
||||
@@ -234,8 +238,20 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mfa.toastEnabled': 'Kétfaktoros hitelesítés engedélyezve',
|
||||
'settings.mfa.toastDisabled': 'Kétfaktoros hitelesítés kikapcsolva',
|
||||
'settings.mfa.demoBlocked': 'Demo módban nem érhető el',
|
||||
'admin.notifications.title': 'Értesítések',
|
||||
'admin.notifications.hint': 'Válasszon értesítési csatornát. Egyszerre csak egy lehet aktív.',
|
||||
'admin.notifications.none': 'Kikapcsolva',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Értesítési események',
|
||||
'admin.notifications.eventsHint': 'Válaszd ki, mely események indítsanak értesítéseket minden felhasználó számára.',
|
||||
'admin.notifications.save': 'Értesítési beállítások mentése',
|
||||
'admin.notifications.saved': 'Értesítési beállítások mentve',
|
||||
'admin.notifications.testWebhook': 'Teszt webhook küldése',
|
||||
'admin.notifications.testWebhookSuccess': 'Teszt webhook sikeresen elküldve',
|
||||
'admin.notifications.testWebhookFailed': 'Teszt webhook küldése sikertelen',
|
||||
'admin.smtp.title': 'E-mail és értesítések',
|
||||
'admin.smtp.hint': 'SMTP konfiguráció e-mail értesítésekhez. Opcionális: Webhook URL Discordhoz, Slackhez stb.',
|
||||
'admin.smtp.hint': 'SMTP konfiguráció e-mail értesítések küldéséhez.',
|
||||
'admin.smtp.testButton': 'Teszt e-mail küldése',
|
||||
'admin.smtp.testSuccess': 'Teszt e-mail sikeresen elküldve',
|
||||
'admin.smtp.testFailed': 'Teszt e-mail küldése sikertelen',
|
||||
@@ -296,6 +312,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Bejelentkezés',
|
||||
'login.createAdmin': 'Admin fiók létrehozása',
|
||||
'login.createAdminHint': 'Hozd létre az első admin fiókot a TREK-hez.',
|
||||
'login.setNewPassword': 'Új jelszó beállítása',
|
||||
'login.setNewPasswordHint': 'A folytatás előtt meg kell változtatnia a jelszavát.',
|
||||
'login.createAccount': 'Fiók létrehozása',
|
||||
'login.createAccountHint': 'Új fiók regisztrálása.',
|
||||
'login.creating': 'Létrehozás…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Password',
|
||||
'common.saving': 'Salvataggio...',
|
||||
'common.saved': 'Salvato',
|
||||
'common.update': 'Aggiorna',
|
||||
'common.change': 'Cambia',
|
||||
'common.uploading': 'Caricamento…',
|
||||
@@ -149,6 +150,9 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.notifyCollabMessage': 'Messaggi chat (Collab)',
|
||||
'settings.notifyPackingTagged': 'Lista valigia: assegnazioni',
|
||||
'settings.notifyWebhook': 'Notifiche webhook',
|
||||
'settings.notificationsDisabled': 'Le notifiche non sono configurate. Chiedi a un amministratore di abilitare le notifiche e-mail o webhook.',
|
||||
'settings.notificationsActive': 'Canale attivo',
|
||||
'settings.notificationsManagedByAdmin': 'Gli eventi di notifica sono configurati dall\'amministratore.',
|
||||
'settings.on': 'On',
|
||||
'settings.off': 'Off',
|
||||
'settings.mcp.title': 'Configurazione MCP',
|
||||
@@ -234,8 +238,20 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'settings.mfa.toastEnabled': 'Autenticazione a due fattori abilitata',
|
||||
'settings.mfa.toastDisabled': 'Autenticazione a due fattori disabilitata',
|
||||
'settings.mfa.demoBlocked': 'Non disponibile in modalità demo',
|
||||
'admin.notifications.title': 'Notifiche',
|
||||
'admin.notifications.hint': 'Scegli un canale di notifica. Solo uno può essere attivo alla volta.',
|
||||
'admin.notifications.none': 'Disattivato',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Eventi di notifica',
|
||||
'admin.notifications.eventsHint': 'Scegli quali eventi attivano le notifiche per tutti gli utenti.',
|
||||
'admin.notifications.save': 'Salva impostazioni notifiche',
|
||||
'admin.notifications.saved': 'Impostazioni notifiche salvate',
|
||||
'admin.notifications.testWebhook': 'Invia webhook di test',
|
||||
'admin.notifications.testWebhookSuccess': 'Webhook di test inviato con successo',
|
||||
'admin.notifications.testWebhookFailed': 'Invio webhook di test fallito',
|
||||
'admin.smtp.title': 'Email e notifiche',
|
||||
'admin.smtp.hint': 'Configurazione SMTP per le notifiche via email. Opzionale: URL webhook per Discord, Slack, ecc.',
|
||||
'admin.smtp.hint': 'Configurazione SMTP per l\'invio delle notifiche via e-mail.',
|
||||
'admin.smtp.testButton': 'Invia email di prova',
|
||||
'admin.smtp.testSuccess': 'Email di prova inviata con successo',
|
||||
'admin.smtp.testFailed': 'Invio email di prova fallito',
|
||||
@@ -296,6 +312,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'login.signIn': 'Accedi',
|
||||
'login.createAdmin': 'Crea Account Amministratore',
|
||||
'login.createAdminHint': 'Imposta il primo account amministratore per TREK.',
|
||||
'login.setNewPassword': 'Imposta nuova password',
|
||||
'login.setNewPasswordHint': 'Devi cambiare la password prima di continuare.',
|
||||
'login.createAccount': 'Crea Account',
|
||||
'login.createAccountHint': 'Registra un nuovo account.',
|
||||
'login.creating': 'Creazione in corso…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const nl: Record<string, string> = {
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Wachtwoord',
|
||||
'common.saving': 'Opslaan...',
|
||||
'common.saved': 'Opgeslagen',
|
||||
'common.update': 'Bijwerken',
|
||||
'common.change': 'Wijzigen',
|
||||
'common.uploading': 'Uploaden…',
|
||||
@@ -149,8 +150,23 @@ const nl: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': 'Chatberichten (Collab)',
|
||||
'settings.notifyPackingTagged': 'Paklijst: toewijzingen',
|
||||
'settings.notifyWebhook': 'Webhook-meldingen',
|
||||
'settings.notificationsDisabled': 'Meldingen zijn niet geconfigureerd. Vraag een beheerder om e-mail- of webhookmeldingen in te schakelen.',
|
||||
'settings.notificationsActive': 'Actief kanaal',
|
||||
'settings.notificationsManagedByAdmin': 'Meldingsgebeurtenissen worden geconfigureerd door je beheerder.',
|
||||
'admin.notifications.title': 'Meldingen',
|
||||
'admin.notifications.hint': 'Kies een meldingskanaal. Er kan er slechts één tegelijk actief zijn.',
|
||||
'admin.notifications.none': 'Uitgeschakeld',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'Meldingsgebeurtenissen',
|
||||
'admin.notifications.eventsHint': 'Kies welke gebeurtenissen meldingen activeren voor alle gebruikers.',
|
||||
'admin.notifications.save': 'Meldingsinstellingen opslaan',
|
||||
'admin.notifications.saved': 'Meldingsinstellingen opgeslagen',
|
||||
'admin.notifications.testWebhook': 'Testwebhook verzenden',
|
||||
'admin.notifications.testWebhookSuccess': 'Testwebhook succesvol verzonden',
|
||||
'admin.notifications.testWebhookFailed': 'Testwebhook mislukt',
|
||||
'admin.smtp.title': 'E-mail en meldingen',
|
||||
'admin.smtp.hint': 'SMTP-configuratie voor e-mailmeldingen. Optioneel: Webhook-URL voor Discord, Slack, etc.',
|
||||
'admin.smtp.hint': 'SMTP-configuratie voor het verzenden van e-mailmeldingen.',
|
||||
'admin.smtp.testButton': 'Test-e-mail verzenden',
|
||||
'admin.smtp.testSuccess': 'Test-e-mail succesvol verzonden',
|
||||
'admin.smtp.testFailed': 'Test-e-mail mislukt',
|
||||
@@ -296,6 +312,8 @@ const nl: Record<string, string> = {
|
||||
'login.signIn': 'Inloggen',
|
||||
'login.createAdmin': 'Beheerdersaccount aanmaken',
|
||||
'login.createAdminHint': 'Stel het eerste beheerdersaccount in voor TREK.',
|
||||
'login.setNewPassword': 'Nieuw wachtwoord instellen',
|
||||
'login.setNewPasswordHint': 'U moet uw wachtwoord wijzigen voordat u verder kunt gaan.',
|
||||
'login.createAccount': 'Account aanmaken',
|
||||
'login.createAccountHint': 'Registreer een nieuw account.',
|
||||
'login.creating': 'Aanmaken…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const ru: Record<string, string> = {
|
||||
'common.email': 'Эл. почта',
|
||||
'common.password': 'Пароль',
|
||||
'common.saving': 'Сохранение...',
|
||||
'common.saved': 'Сохранено',
|
||||
'common.update': 'Обновить',
|
||||
'common.change': 'Изменить',
|
||||
'common.uploading': 'Загрузка…',
|
||||
@@ -149,8 +150,23 @@ const ru: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': 'Сообщения чата (Collab)',
|
||||
'settings.notifyPackingTagged': 'Список вещей: назначения',
|
||||
'settings.notifyWebhook': 'Webhook-уведомления',
|
||||
'settings.notificationsDisabled': 'Уведомления не настроены. Попросите администратора включить уведомления по электронной почте или webhook.',
|
||||
'settings.notificationsActive': 'Активный канал',
|
||||
'settings.notificationsManagedByAdmin': 'События уведомлений настраиваются администратором.',
|
||||
'admin.notifications.title': 'Уведомления',
|
||||
'admin.notifications.hint': 'Выберите канал уведомлений. Одновременно может быть активен только один.',
|
||||
'admin.notifications.none': 'Отключено',
|
||||
'admin.notifications.email': 'Эл. почта (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': 'События уведомлений',
|
||||
'admin.notifications.eventsHint': 'Выберите, какие события вызывают уведомления для всех пользователей.',
|
||||
'admin.notifications.save': 'Сохранить настройки уведомлений',
|
||||
'admin.notifications.saved': 'Настройки уведомлений сохранены',
|
||||
'admin.notifications.testWebhook': 'Отправить тестовый вебхук',
|
||||
'admin.notifications.testWebhookSuccess': 'Тестовый вебхук успешно отправлен',
|
||||
'admin.notifications.testWebhookFailed': 'Ошибка отправки тестового вебхука',
|
||||
'admin.smtp.title': 'Почта и уведомления',
|
||||
'admin.smtp.hint': 'Настройка SMTP для уведомлений по почте. Необязательно: Webhook URL для Discord, Slack и т.д.',
|
||||
'admin.smtp.hint': 'Конфигурация SMTP для отправки уведомлений по электронной почте.',
|
||||
'admin.smtp.testButton': 'Отправить тестовое письмо',
|
||||
'admin.smtp.testSuccess': 'Тестовое письмо успешно отправлено',
|
||||
'admin.smtp.testFailed': 'Ошибка отправки тестового письма',
|
||||
@@ -296,6 +312,8 @@ const ru: Record<string, string> = {
|
||||
'login.signIn': 'Войти',
|
||||
'login.createAdmin': 'Создать аккаунт администратора',
|
||||
'login.createAdminHint': 'Настройте первый аккаунт администратора для TREK.',
|
||||
'login.setNewPassword': 'Установить новый пароль',
|
||||
'login.setNewPasswordHint': 'Вы должны сменить пароль, прежде чем продолжить.',
|
||||
'login.createAccount': 'Создать аккаунт',
|
||||
'login.createAccountHint': 'Зарегистрируйте новый аккаунт.',
|
||||
'login.creating': 'Создание…',
|
||||
|
||||
@@ -25,6 +25,7 @@ const zh: Record<string, string> = {
|
||||
'common.email': '邮箱',
|
||||
'common.password': '密码',
|
||||
'common.saving': '保存中...',
|
||||
'common.saved': '已保存',
|
||||
'common.update': '更新',
|
||||
'common.change': '修改',
|
||||
'common.uploading': '上传中…',
|
||||
@@ -149,8 +150,23 @@ const zh: Record<string, string> = {
|
||||
'settings.notifyCollabMessage': '聊天消息 (Collab)',
|
||||
'settings.notifyPackingTagged': '行李清单:分配',
|
||||
'settings.notifyWebhook': 'Webhook 通知',
|
||||
'settings.notificationsDisabled': '通知尚未配置。请联系管理员启用电子邮件或 Webhook 通知。',
|
||||
'settings.notificationsActive': '活跃频道',
|
||||
'settings.notificationsManagedByAdmin': '通知事件由管理员配置。',
|
||||
'admin.notifications.title': '通知',
|
||||
'admin.notifications.hint': '选择一个通知渠道。一次只能激活一个。',
|
||||
'admin.notifications.none': '已禁用',
|
||||
'admin.notifications.email': '电子邮件 (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.events': '通知事件',
|
||||
'admin.notifications.eventsHint': '选择哪些事件为所有用户触发通知。',
|
||||
'admin.notifications.save': '保存通知设置',
|
||||
'admin.notifications.saved': '通知设置已保存',
|
||||
'admin.notifications.testWebhook': '发送测试 Webhook',
|
||||
'admin.notifications.testWebhookSuccess': '测试 Webhook 发送成功',
|
||||
'admin.notifications.testWebhookFailed': '测试 Webhook 发送失败',
|
||||
'admin.smtp.title': '邮件与通知',
|
||||
'admin.smtp.hint': '用于邮件通知的 SMTP 配置。可选:Discord、Slack 等的 Webhook URL。',
|
||||
'admin.smtp.hint': '用于发送电子邮件通知的 SMTP 配置。',
|
||||
'admin.smtp.testButton': '发送测试邮件',
|
||||
'admin.smtp.testSuccess': '测试邮件发送成功',
|
||||
'admin.smtp.testFailed': '测试邮件发送失败',
|
||||
@@ -296,6 +312,8 @@ const zh: Record<string, string> = {
|
||||
'login.signIn': '登录',
|
||||
'login.createAdmin': '创建管理员账户',
|
||||
'login.createAdminHint': '为 TREK 设置第一个管理员账户。',
|
||||
'login.setNewPassword': '设置新密码',
|
||||
'login.setNewPasswordHint': '您必须更改密码才能继续。',
|
||||
'login.createAccount': '创建账户',
|
||||
'login.createAccountHint': '注册新账户。',
|
||||
'login.creating': '创建中…',
|
||||
|
||||
@@ -974,64 +974,172 @@ export default function AdminPage(): React.ReactElement {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* SMTP / Notifications */}
|
||||
{/* Notifications — exclusive channel selector */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-100">
|
||||
<h2 className="font-semibold text-slate-900">{t('admin.smtp.title')}</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">{t('admin.smtp.hint')}</p>
|
||||
<h2 className="font-semibold text-slate-900">{t('admin.notifications.title')}</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">{t('admin.notifications.hint')}</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-3">
|
||||
{smtpLoaded && [
|
||||
{ key: 'smtp_host', label: 'SMTP Host', placeholder: 'mail.example.com' },
|
||||
{ key: 'smtp_port', label: 'SMTP Port', placeholder: '587' },
|
||||
{ key: 'smtp_user', label: 'SMTP User', placeholder: 'trek@example.com' },
|
||||
{ key: 'smtp_pass', label: 'SMTP Password', placeholder: '••••••••', type: 'password' },
|
||||
{ key: 'smtp_from', label: 'From Address', placeholder: 'trek@example.com' },
|
||||
{ key: 'notification_webhook_url', label: 'Webhook URL (optional)', placeholder: 'https://discord.com/api/webhooks/...' },
|
||||
{ key: 'app_url', label: 'App URL (for email links)', placeholder: 'https://trek.example.com' },
|
||||
].map(field => (
|
||||
<div key={field.key}>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{field.label}</label>
|
||||
<input
|
||||
type={field.type || 'text'}
|
||||
value={smtpValues[field.key] || ''}
|
||||
onChange={e => setSmtpValues(prev => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder}
|
||||
onBlur={e => { if (e.target.value !== '') authApi.updateAppSettings({ [field.key]: e.target.value }).then(() => toast.success(t('common.saved'))).catch(() => {}) }}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* Skip TLS toggle */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0' }}>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-slate-500">Skip TLS certificate check</span>
|
||||
<p className="text-[10px] text-slate-400 mt-0.5">Enable for self-signed certificates on local mail servers</p>
|
||||
</div>
|
||||
<button onClick={async () => {
|
||||
const newVal = smtpValues.smtp_skip_tls_verify === 'true' ? 'false' : 'true'
|
||||
setSmtpValues(prev => ({ ...prev, smtp_skip_tls_verify: newVal }))
|
||||
await authApi.updateAppSettings({ smtp_skip_tls_verify: newVal }).catch(() => {})
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${smtpValues.smtp_skip_tls_verify === 'true' ? 'bg-slate-900' : 'bg-slate-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${smtpValues.smtp_skip_tls_verify === 'true' ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Channel selector */}
|
||||
<div className="flex gap-2">
|
||||
{(['none', 'email', 'webhook'] as const).map(ch => {
|
||||
const active = (smtpValues.notification_channel || 'none') === ch
|
||||
const labels: Record<string, string> = { none: t('admin.notifications.none'), email: t('admin.notifications.email'), webhook: t('admin.notifications.webhook') }
|
||||
return (
|
||||
<button
|
||||
key={ch}
|
||||
onClick={() => setSmtpValues(prev => ({ ...prev, notification_channel: ch }))}
|
||||
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-colors border ${active ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-600 border-slate-300 hover:bg-slate-50'}`}
|
||||
>
|
||||
{labels[ch]}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Notification event toggles — shown when any channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') !== 'none' && (
|
||||
<div className="space-y-2 pt-2 border-t border-slate-100">
|
||||
<p className="text-xs font-medium text-slate-500 uppercase tracking-wider mb-2">{t('admin.notifications.events')}</p>
|
||||
<p className="text-[10px] text-slate-400 mb-3">{t('admin.notifications.eventsHint')}</p>
|
||||
{[
|
||||
{ key: 'notify_trip_invite', label: t('settings.notifyTripInvite') },
|
||||
{ key: 'notify_booking_change', label: t('settings.notifyBookingChange') },
|
||||
{ key: 'notify_trip_reminder', label: t('settings.notifyTripReminder') },
|
||||
{ key: 'notify_vacay_invite', label: t('settings.notifyVacayInvite') },
|
||||
{ key: 'notify_photos_shared', label: t('settings.notifyPhotosShared') },
|
||||
{ key: 'notify_collab_message', label: t('settings.notifyCollabMessage') },
|
||||
{ key: 'notify_packing_tagged', label: t('settings.notifyPackingTagged') },
|
||||
].map(opt => {
|
||||
const isOn = (smtpValues[opt.key] ?? 'true') !== 'false'
|
||||
return (
|
||||
<div key={opt.key} className="flex items-center justify-between py-1">
|
||||
<span className="text-sm text-slate-700">{opt.label}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newVal = isOn ? 'false' : 'true'
|
||||
setSmtpValues(prev => ({ ...prev, [opt.key]: newVal }))
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isOn ? 'bg-slate-900' : 'bg-slate-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${isOn ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email (SMTP) settings — shown when email channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') === 'email' && (
|
||||
<div className="space-y-3 pt-2 border-t border-slate-100">
|
||||
<p className="text-xs text-slate-400">{t('admin.smtp.hint')}</p>
|
||||
{smtpLoaded && [
|
||||
{ key: 'smtp_host', label: 'SMTP Host', placeholder: 'mail.example.com' },
|
||||
{ key: 'smtp_port', label: 'SMTP Port', placeholder: '587' },
|
||||
{ key: 'smtp_user', label: 'SMTP User', placeholder: 'trek@example.com' },
|
||||
{ key: 'smtp_pass', label: 'SMTP Password', placeholder: '••••••••', type: 'password' },
|
||||
{ key: 'smtp_from', label: 'From Address', placeholder: 'trek@example.com' },
|
||||
].map(field => (
|
||||
<div key={field.key}>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{field.label}</label>
|
||||
<input
|
||||
type={field.type || 'text'}
|
||||
value={smtpValues[field.key] || ''}
|
||||
onChange={e => setSmtpValues(prev => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0' }}>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-slate-500">Skip TLS certificate check</span>
|
||||
<p className="text-[10px] text-slate-400 mt-0.5">Enable for self-signed certificates on local mail servers</p>
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
const newVal = smtpValues.smtp_skip_tls_verify === 'true' ? 'false' : 'true'
|
||||
setSmtpValues(prev => ({ ...prev, smtp_skip_tls_verify: newVal }))
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${smtpValues.smtp_skip_tls_verify === 'true' ? 'bg-slate-900' : 'bg-slate-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${smtpValues.smtp_skip_tls_verify === 'true' ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Webhook settings — shown when webhook channel is active */}
|
||||
{(smtpValues.notification_channel || 'none') === 'webhook' && (
|
||||
<div className="space-y-3 pt-2 border-t border-slate-100">
|
||||
<p className="text-xs text-slate-400">{t('admin.webhook.hint') || 'Send notifications to an external webhook (Discord, Slack, etc.).'}</p>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">Webhook URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={smtpValues.notification_webhook_url || ''}
|
||||
onChange={e => setSmtpValues(prev => ({ ...prev, notification_webhook_url: e.target.value }))}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-400 mt-1">TREK will POST JSON with event, title, body, and timestamp to this URL.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save + Test buttons */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-slate-100">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const notifKeys = ['notification_channel', 'notification_webhook_url', 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from', 'smtp_skip_tls_verify', 'notify_trip_invite', 'notify_booking_change', 'notify_trip_reminder', 'notify_vacay_invite', 'notify_photos_shared', 'notify_collab_message', 'notify_packing_tagged']
|
||||
const payload: Record<string, string> = {}
|
||||
for (const k of notifKeys) { if (smtpValues[k] !== undefined) payload[k] = smtpValues[k] }
|
||||
try {
|
||||
await authApi.updateAppSettings(payload)
|
||||
toast.success(t('admin.notifications.saved'))
|
||||
} catch { toast.error(t('common.error')) }
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{t('common.save')}
|
||||
</button>
|
||||
{(smtpValues.notification_channel || 'none') === 'email' && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const smtpKeys = ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from', 'smtp_skip_tls_verify']
|
||||
const payload: Record<string, string> = {}
|
||||
for (const k of smtpKeys) { if (smtpValues[k]) payload[k] = smtpValues[k] }
|
||||
await authApi.updateAppSettings(payload).catch(() => {})
|
||||
try {
|
||||
const result = await notificationsApi.testSmtp()
|
||||
if (result.success) toast.success(t('admin.smtp.testSuccess'))
|
||||
else toast.error(result.error || t('admin.smtp.testFailed'))
|
||||
} catch { toast.error(t('admin.smtp.testFailed')) }
|
||||
}}
|
||||
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
{t('admin.smtp.testButton')}
|
||||
</button>
|
||||
)}
|
||||
{(smtpValues.notification_channel || 'none') === 'webhook' && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (smtpValues.notification_webhook_url) {
|
||||
await authApi.updateAppSettings({ notification_webhook_url: smtpValues.notification_webhook_url }).catch(() => {})
|
||||
}
|
||||
try {
|
||||
const result = await notificationsApi.testWebhook()
|
||||
if (result.success) toast.success(t('admin.notifications.testWebhookSuccess'))
|
||||
else toast.error(result.error || t('admin.notifications.testWebhookFailed'))
|
||||
} catch { toast.error(t('admin.notifications.testWebhookFailed')) }
|
||||
}}
|
||||
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
{t('admin.notifications.testWebhook')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
for (const k of ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from']) {
|
||||
if (smtpValues[k]) await authApi.updateAppSettings({ [k]: smtpValues[k] }).catch(() => {})
|
||||
}
|
||||
try {
|
||||
const result = await notificationsApi.testSmtp()
|
||||
if (result.success) toast.success(t('admin.smtp.testSuccess'))
|
||||
else toast.error(result.error || t('admin.smtp.testFailed'))
|
||||
} catch { toast.error(t('admin.smtp.testFailed')) }
|
||||
}}
|
||||
className="px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
{t('admin.smtp.testButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1039,7 +1147,7 @@ export default function AdminPage(): React.ReactElement {
|
||||
|
||||
{activeTab === 'backup' && <BackupPanel />}
|
||||
|
||||
{activeTab === 'audit' && <AuditLogPanel />}
|
||||
{activeTab === 'audit' && <AuditLogPanel serverTimezone={serverTimezone} />}
|
||||
|
||||
{activeTab === 'mcp-tokens' && <AdminMcpTokensPanel />}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe,
|
||||
interface AppConfig {
|
||||
has_users: boolean
|
||||
allow_registration: boolean
|
||||
setup_complete: boolean
|
||||
demo_mode: boolean
|
||||
oidc_configured: boolean
|
||||
oidc_display_name?: string
|
||||
@@ -28,7 +29,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
const [inviteToken, setInviteToken] = useState<string>('')
|
||||
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
||||
|
||||
const { login, register, demoLogin, completeMfaLogin } = useAuthStore()
|
||||
const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore()
|
||||
const { setLanguageLocal } = useSettingsStore()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -110,19 +111,39 @@ export default function LoginPage(): React.ReactElement {
|
||||
const [mfaStep, setMfaStep] = useState(false)
|
||||
const [mfaToken, setMfaToken] = useState('')
|
||||
const [mfaCode, setMfaCode] = useState('')
|
||||
const [passwordChangeStep, setPasswordChangeStep] = useState(false)
|
||||
const [savedLoginPassword, setSavedLoginPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (passwordChangeStep) {
|
||||
if (!newPassword) { setError(t('settings.passwordRequired')); setIsLoading(false); return }
|
||||
if (newPassword.length < 8) { setError(t('settings.passwordTooShort')); setIsLoading(false); return }
|
||||
if (newPassword !== confirmPassword) { setError(t('settings.passwordMismatch')); setIsLoading(false); return }
|
||||
await authApi.changePassword({ current_password: savedLoginPassword, new_password: newPassword })
|
||||
await loadUser({ silent: true })
|
||||
setShowTakeoff(true)
|
||||
setTimeout(() => navigate('/dashboard'), 2600)
|
||||
return
|
||||
}
|
||||
if (mode === 'login' && mfaStep) {
|
||||
if (!mfaCode.trim()) {
|
||||
setError(t('login.mfaCodeRequired'))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
await completeMfaLogin(mfaToken, mfaCode)
|
||||
const mfaResult = await completeMfaLogin(mfaToken, mfaCode)
|
||||
if ('user' in mfaResult && mfaResult.user?.must_change_password) {
|
||||
setSavedLoginPassword(password)
|
||||
setPasswordChangeStep(true)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
setShowTakeoff(true)
|
||||
setTimeout(() => navigate('/dashboard'), 2600)
|
||||
return
|
||||
@@ -140,6 +161,12 @@ export default function LoginPage(): React.ReactElement {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if ('user' in result && result.user?.must_change_password) {
|
||||
setSavedLoginPassword(password)
|
||||
setPasswordChangeStep(true)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
setShowTakeoff(true)
|
||||
setTimeout(() => navigate('/dashboard'), 2600)
|
||||
@@ -149,7 +176,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
const showRegisterOption = (appConfig?.allow_registration || !appConfig?.has_users || inviteValid) && !appConfig?.oidc_only_mode
|
||||
const showRegisterOption = (appConfig?.allow_registration || !appConfig?.has_users || inviteValid) && !appConfig?.oidc_only_mode && (appConfig?.setup_complete !== false || !appConfig?.has_users)
|
||||
|
||||
// In OIDC-only mode, show a minimal page that redirects directly to the IdP
|
||||
const oidcOnly = appConfig?.oidc_only_mode && appConfig?.oidc_configured
|
||||
@@ -516,18 +543,22 @@ export default function LoginPage(): React.ReactElement {
|
||||
) : (
|
||||
<>
|
||||
<h2 style={{ margin: '0 0 4px', fontSize: 22, fontWeight: 800, color: '#111827' }}>
|
||||
{mode === 'login' && mfaStep
|
||||
? t('login.mfaTitle')
|
||||
: mode === 'register'
|
||||
? (!appConfig?.has_users ? t('login.createAdmin') : t('login.createAccount'))
|
||||
: t('login.title')}
|
||||
{passwordChangeStep
|
||||
? t('login.setNewPassword')
|
||||
: mode === 'login' && mfaStep
|
||||
? t('login.mfaTitle')
|
||||
: mode === 'register'
|
||||
? (!appConfig?.has_users ? t('login.createAdmin') : t('login.createAccount'))
|
||||
: t('login.title')}
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 28px', fontSize: 13.5, color: '#9ca3af' }}>
|
||||
{mode === 'login' && mfaStep
|
||||
? t('login.mfaSubtitle')
|
||||
: mode === 'register'
|
||||
? (!appConfig?.has_users ? t('login.createAdminHint') : t('login.createAccountHint'))
|
||||
: t('login.subtitle')}
|
||||
{passwordChangeStep
|
||||
? t('login.setNewPasswordHint')
|
||||
: mode === 'login' && mfaStep
|
||||
? t('login.mfaSubtitle')
|
||||
: mode === 'register'
|
||||
? (!appConfig?.has_users ? t('login.createAdminHint') : t('login.createAccountHint'))
|
||||
: t('login.subtitle')}
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
@@ -537,7 +568,39 @@ export default function LoginPage(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'login' && mfaStep && (
|
||||
{passwordChangeStep && (
|
||||
<>
|
||||
<div style={{ padding: '10px 14px', background: '#fefce8', border: '1px solid #fde68a', borderRadius: 10, fontSize: 13, color: '#92400e' }}>
|
||||
{t('settings.mustChangePassword')}
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.newPassword')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} />
|
||||
<input
|
||||
type="password" value={newPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewPassword(e.target.value)} required
|
||||
placeholder={t('settings.newPassword')} style={inputBase}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => e.target.style.borderColor = '#111827'}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('settings.confirmPassword')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Lock size={15} style={{ position: 'absolute', left: 13, top: '50%', transform: 'translateY(-50%)', color: '#9ca3af', pointerEvents: 'none' }} />
|
||||
<input
|
||||
type="password" value={confirmPassword} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmPassword(e.target.value)} required
|
||||
placeholder={t('settings.confirmPassword')} style={inputBase}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => e.target.style.borderColor = '#111827'}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === 'login' && mfaStep && !passwordChangeStep && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.mfaCodeLabel')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -567,7 +630,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
)}
|
||||
|
||||
{/* Username (register only) */}
|
||||
{mode === 'register' && (
|
||||
{mode === 'register' && !passwordChangeStep && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('login.username')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -583,7 +646,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
)}
|
||||
|
||||
{/* Email */}
|
||||
{!(mode === 'login' && mfaStep) && (
|
||||
{!(mode === 'login' && mfaStep) && !passwordChangeStep && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.email')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -599,7 +662,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
)}
|
||||
|
||||
{/* Password */}
|
||||
{!(mode === 'login' && mfaStep) && (
|
||||
{!(mode === 'login' && mfaStep) && !passwordChangeStep && (
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: 12.5, fontWeight: 600, color: '#374151', marginBottom: 6 }}>{t('common.password')}</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -630,14 +693,14 @@ export default function LoginPage(): React.ReactElement {
|
||||
onMouseLeave={(e: React.MouseEvent<HTMLButtonElement>) => e.currentTarget.style.background = '#111827'}
|
||||
>
|
||||
{isLoading
|
||||
? <><div style={{ width: 15, height: 15, border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />{mode === 'register' ? t('login.creating') : (mode === 'login' && mfaStep ? t('login.mfaVerify') : t('login.signingIn'))}</>
|
||||
: <><Plane size={16} />{mode === 'register' ? t('login.createAccount') : (mode === 'login' && mfaStep ? t('login.mfaVerify') : t('login.signIn'))}</>
|
||||
? <><div style={{ width: 15, height: 15, border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />{passwordChangeStep ? t('settings.updatePassword') : mode === 'register' ? t('login.creating') : (mode === 'login' && mfaStep ? t('login.mfaVerify') : t('login.signingIn'))}</>
|
||||
: <><Plane size={16} />{passwordChangeStep ? t('settings.updatePassword') : mode === 'register' ? t('login.createAccount') : (mode === 'login' && mfaStep ? t('login.mfaVerify') : t('login.signIn'))}</>
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Toggle login/register */}
|
||||
{showRegisterOption && appConfig?.has_users && !appConfig?.demo_mode && (
|
||||
{showRegisterOption && appConfig?.has_users && !appConfig?.demo_mode && !passwordChangeStep && (
|
||||
<p style={{ textAlign: 'center', marginTop: 16, fontSize: 13, color: '#9ca3af' }}>
|
||||
{mode === 'login' ? t('login.noAccount') + ' ' : t('login.hasAccount') + ' '}
|
||||
<button onClick={() => { setMode(m => m === 'login' ? 'register' : 'login'); setError(''); setMfaStep(false); setMfaToken(''); setMfaCode('') }}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Navbar from '../components/Layout/Navbar'
|
||||
import CustomSelect from '../components/shared/CustomSelect'
|
||||
import { useToast } from '../components/shared/Toast'
|
||||
import { Save, Map, Palette, User, Moon, Sun, Monitor, Shield, Camera, Trash2, Lock, KeyRound, AlertTriangle, Copy, Download, Printer, Terminal, Plus, Check } from 'lucide-react'
|
||||
import { authApi, adminApi, notificationsApi } from '../api/client'
|
||||
import { authApi, adminApi } from '../api/client'
|
||||
import apiClient from '../api/client'
|
||||
import { useAddonStore } from '../store/addonStore'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
@@ -56,56 +56,54 @@ function Section({ title, icon: Icon, children }: SectionProps): React.ReactElem
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationPreferences({ t, memoriesEnabled }: { t: any; memoriesEnabled: boolean }) {
|
||||
const [prefs, setPrefs] = useState<Record<string, number> | null>(null)
|
||||
const [addons, setAddons] = useState<Record<string, boolean>>({})
|
||||
useEffect(() => { notificationsApi.getPreferences().then(d => setPrefs(d.preferences)).catch(() => {}) }, [])
|
||||
function ToggleSwitch({ on, onToggle }: { on: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<button onClick={onToggle}
|
||||
style={{
|
||||
position: 'relative', width: 44, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer',
|
||||
background: on ? 'var(--accent, #111827)' : 'var(--border-primary, #d1d5db)',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: on ? 22 : 2,
|
||||
width: 20, height: 20, borderRadius: '50%', background: 'white',
|
||||
transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationPreferences({ t }: { t: any; memoriesEnabled: boolean }) {
|
||||
const [notifChannel, setNotifChannel] = useState<string>('none')
|
||||
useEffect(() => {
|
||||
apiClient.get('/addons').then(r => {
|
||||
const map: Record<string, boolean> = {}
|
||||
for (const a of (r.data.addons || [])) map[a.id] = !!a.enabled
|
||||
setAddons(map)
|
||||
authApi.getAppConfig?.().then((cfg: any) => {
|
||||
if (cfg?.notification_channel) setNotifChannel(cfg.notification_channel)
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const toggle = async (key: string) => {
|
||||
if (!prefs) return
|
||||
const newVal = prefs[key] ? 0 : 1
|
||||
setPrefs(prev => prev ? { ...prev, [key]: newVal } : prev)
|
||||
try { await notificationsApi.updatePreferences({ [key]: !!newVal }) } catch {}
|
||||
if (notifChannel === 'none') {
|
||||
return (
|
||||
<p style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic' }}>
|
||||
{t('settings.notificationsDisabled')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (!prefs) return <p style={{ fontSize: 12, color: 'var(--text-faint)' }}>{t('common.loading')}</p>
|
||||
|
||||
const options = [
|
||||
{ key: 'notify_trip_invite', label: t('settings.notifyTripInvite') },
|
||||
{ key: 'notify_booking_change', label: t('settings.notifyBookingChange') },
|
||||
...(addons.vacay ? [{ key: 'notify_vacay_invite', label: t('settings.notifyVacayInvite') }] : []),
|
||||
...(memoriesEnabled ? [{ key: 'notify_photos_shared', label: t('settings.notifyPhotosShared') }] : []),
|
||||
...(addons.collab ? [{ key: 'notify_collab_message', label: t('settings.notifyCollabMessage') }] : []),
|
||||
...(addons.documents ? [{ key: 'notify_packing_tagged', label: t('settings.notifyPackingTagged') }] : []),
|
||||
{ key: 'notify_webhook', label: t('settings.notifyWebhook') },
|
||||
]
|
||||
const channelLabel = notifChannel === 'email'
|
||||
? (t('admin.notifications.email') || 'Email (SMTP)')
|
||||
: (t('admin.notifications.webhook') || 'Webhook')
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{options.map(opt => (
|
||||
<div key={opt.key} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-primary)' }}>{opt.label}</span>
|
||||
<button onClick={() => toggle(opt.key)}
|
||||
style={{
|
||||
position: 'relative', width: 44, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer',
|
||||
background: prefs[opt.key] ? 'var(--accent, #111827)' : 'var(--border-primary, #d1d5db)',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: prefs[opt.key] ? 22 : 2,
|
||||
width: 20, height: 20, borderRadius: '50%', background: 'white',
|
||||
transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#22c55e', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 13, color: 'var(--text-primary)', fontWeight: 500 }}>
|
||||
{t('settings.notificationsActive')}: {channelLabel}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: 'var(--text-faint)', margin: 0, lineHeight: 1.5 }}>
|
||||
{t('settings.notificationsManagedByAdmin')}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -924,6 +922,7 @@ export default function SettingsPage(): React.ReactElement {
|
||||
await authApi.changePassword({ current_password: currentPassword, new_password: newPassword })
|
||||
toast.success(t('settings.passwordChanged'))
|
||||
setCurrentPassword(''); setNewPassword(''); setConfirmPassword('')
|
||||
await loadUser({ silent: true })
|
||||
} catch (err: unknown) {
|
||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface User {
|
||||
created_at: string
|
||||
/** Present after load; true when TOTP MFA is enabled for password login */
|
||||
mfa_enabled?: boolean
|
||||
/** True when a password change is required before the user can continue */
|
||||
must_change_password?: boolean
|
||||
}
|
||||
|
||||
export interface Trip {
|
||||
|
||||
Reference in New Issue
Block a user