From da1f300d2d9da314a7dc751b42ce799e0ef5541f Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sat, 22 Nov 2025 00:27:16 +0100 Subject: [PATCH] feat(admin): improve email reminders UX and fix signer deletion Backend changes: - Add SMTPEnabled flag to distinguish SMTP service from MagicLink authentication - Fix URL-encoded email decoding in DELETE /signers/{email} endpoint - Add detailed error logging for expected signer removal operations - Initialize ReminderAsync and MagicLink services unconditionally - Update config tests to reflect new MagicLink requires explicit enabling Frontend changes: - Add ACKIFY_SMTP_ENABLED window variable for feature detection - Hide delete button for expected signers who have already signed - Show email reminders card only when SMTP enabled or history exists - Display informative alert when SMTP disabled but reminder history present - Add i18n translations for email service disabled message (5 languages) These changes improve admin experience by preventing invalid operations (deleting signers who signed, sending emails without SMTP) and providing clear feedback about feature availability. --- .../internal/infrastructure/config/config.go | 13 ++--- .../infrastructure/config/config_test.go | 51 +++++++++++-------- .../presentation/api/admin/handler.go | 20 ++++++-- backend/pkg/web/server.go | 41 ++++++--------- backend/pkg/web/static.go | 12 +++-- docs/en/configuration.md | 22 ++++++++ docs/fr/configuration.md | 22 ++++++++ webapp/index.html | 1 + webapp/src/locales/de.json | 1 + webapp/src/locales/en.json | 1 + webapp/src/locales/es.json | 1 + webapp/src/locales/fr.json | 1 + webapp/src/locales/it.json | 1 + .../src/pages/admin/AdminDocumentDetail.vue | 19 +++++-- 14 files changed, 140 insertions(+), 66 deletions(-) diff --git a/backend/internal/infrastructure/config/config.go b/backend/internal/infrastructure/config/config.go index 5b6e5a7..c1f8c22 100644 --- a/backend/internal/infrastructure/config/config.go +++ b/backend/internal/infrastructure/config/config.go @@ -32,6 +32,7 @@ type AppConfig struct { SecureCookies bool AdminEmails []string OnlyAdminCanCreate bool + SMTPEnabled bool // True if SMTP is configured (for email reminders) } type DatabaseConfig struct { @@ -205,15 +206,9 @@ func Load() (*Config, error) { } } - // Auto-detect MagicLink enabled: true if SMTP is configured - magicLinkConfigured := mailHost != "" - - // Allow manual override via environment variable - if magicLinkEnabledStr := getEnv("ACKIFY_AUTH_MAGICLINK_ENABLED", ""); magicLinkEnabledStr != "" { - config.Auth.MagicLinkEnabled = getEnvBool("ACKIFY_AUTH_MAGICLINK_ENABLED", false) - } else { - config.Auth.MagicLinkEnabled = magicLinkConfigured - } + smtpConfigured := mailHost != "" + config.App.SMTPEnabled = smtpConfigured + config.Auth.MagicLinkEnabled = getEnvBool("ACKIFY_AUTH_MAGICLINK_ENABLED", false) && smtpConfigured // Validation: At least one authentication method must be enabled if !config.Auth.OAuthEnabled && !config.Auth.MagicLinkEnabled { diff --git a/backend/internal/infrastructure/config/config_test.go b/backend/internal/infrastructure/config/config_test.go index 6187ea0..a391d57 100644 --- a/backend/internal/infrastructure/config/config_test.go +++ b/backend/internal/infrastructure/config/config_test.go @@ -1257,11 +1257,12 @@ func TestConfig_AuthValidation(t *testing.T) { { name: "MagicLink only (auto-detected)", envVars: map[string]string{ - "ACKIFY_BASE_URL": "http://localhost:8080", - "ACKIFY_ORGANISATION": "Test Org", - "ACKIFY_DB_DSN": "postgres://localhost/test", - "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), - "ACKIFY_MAIL_HOST": "smtp.example.com", + "ACKIFY_BASE_URL": "http://localhost:8080", + "ACKIFY_ORGANISATION": "Test Org", + "ACKIFY_DB_DSN": "postgres://localhost/test", + "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), + "ACKIFY_MAIL_HOST": "smtp.example.com", + "ACKIFY_AUTH_MAGICLINK_ENABLED": "true", }, expectError: false, checkAuth: func(t *testing.T, cfg *Config) { @@ -1271,19 +1272,23 @@ func TestConfig_AuthValidation(t *testing.T) { if !cfg.Auth.MagicLinkEnabled { t.Error("MagicLink should be enabled") } + if !cfg.App.SMTPEnabled { + t.Error("SMTP should be enabled") + } }, }, { name: "Both OAuth and MagicLink enabled", envVars: map[string]string{ - "ACKIFY_BASE_URL": "http://localhost:8080", - "ACKIFY_ORGANISATION": "Test Org", - "ACKIFY_DB_DSN": "postgres://localhost/test", - "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), - "ACKIFY_OAUTH_CLIENT_ID": "test-client-id", - "ACKIFY_OAUTH_CLIENT_SECRET": "test-secret", - "ACKIFY_OAUTH_PROVIDER": "google", - "ACKIFY_MAIL_HOST": "smtp.example.com", + "ACKIFY_BASE_URL": "http://localhost:8080", + "ACKIFY_ORGANISATION": "Test Org", + "ACKIFY_DB_DSN": "postgres://localhost/test", + "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), + "ACKIFY_OAUTH_CLIENT_ID": "test-client-id", + "ACKIFY_OAUTH_CLIENT_SECRET": "test-secret", + "ACKIFY_OAUTH_PROVIDER": "google", + "ACKIFY_MAIL_HOST": "smtp.example.com", + "ACKIFY_AUTH_MAGICLINK_ENABLED": "true", }, expectError: false, checkAuth: func(t *testing.T, cfg *Config) { @@ -1293,6 +1298,9 @@ func TestConfig_AuthValidation(t *testing.T) { if !cfg.Auth.MagicLinkEnabled { t.Error("MagicLink should be enabled") } + if !cfg.App.SMTPEnabled { + t.Error("SMTP should be enabled") + } }, }, { @@ -1326,14 +1334,15 @@ func TestConfig_AuthValidation(t *testing.T) { { name: "Manual override - disable OAuth even with credentials", envVars: map[string]string{ - "ACKIFY_BASE_URL": "http://localhost:8080", - "ACKIFY_ORGANISATION": "Test Org", - "ACKIFY_DB_DSN": "postgres://localhost/test", - "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), - "ACKIFY_OAUTH_CLIENT_ID": "test-client-id", - "ACKIFY_OAUTH_CLIENT_SECRET": "test-secret", - "ACKIFY_MAIL_HOST": "smtp.example.com", - "ACKIFY_AUTH_OAUTH_ENABLED": "false", + "ACKIFY_BASE_URL": "http://localhost:8080", + "ACKIFY_ORGANISATION": "Test Org", + "ACKIFY_DB_DSN": "postgres://localhost/test", + "ACKIFY_OAUTH_COOKIE_SECRET": base64.StdEncoding.EncodeToString([]byte("test-secret-32-bytes-long!!!!!!")), + "ACKIFY_OAUTH_CLIENT_ID": "test-client-id", + "ACKIFY_OAUTH_CLIENT_SECRET": "test-secret", + "ACKIFY_MAIL_HOST": "smtp.example.com", + "ACKIFY_AUTH_OAUTH_ENABLED": "false", + "ACKIFY_AUTH_MAGICLINK_ENABLED": "true", }, expectError: false, checkAuth: func(t *testing.T, cfg *Config) { diff --git a/backend/internal/presentation/api/admin/handler.go b/backend/internal/presentation/api/admin/handler.go index 13cf526..63b8e09 100644 --- a/backend/internal/presentation/api/admin/handler.go +++ b/backend/internal/presentation/api/admin/handler.go @@ -5,10 +5,12 @@ import ( "context" "encoding/json" "net/http" + "net/url" "github.com/btouchard/ackify-ce/backend/internal/domain/models" "github.com/btouchard/ackify-ce/backend/internal/infrastructure/i18n" "github.com/btouchard/ackify-ce/backend/internal/presentation/api/shared" + "github.com/btouchard/ackify-ce/backend/pkg/logger" "github.com/go-chi/chi/v5" ) @@ -255,7 +257,15 @@ func (h *Handler) HandleAddExpectedSigner(w http.ResponseWriter, r *http.Request func (h *Handler) HandleRemoveExpectedSigner(w http.ResponseWriter, r *http.Request) { ctx := r.Context() docID := chi.URLParam(r, "docId") - email := chi.URLParam(r, "email") + emailEncoded := chi.URLParam(r, "email") + + // Decode URL-encoded email (e.g., al%40bundy.com -> al@bundy.com) + email, err := url.QueryUnescape(emailEncoded) + if err != nil { + logger.Logger.Error("failed to decode email from URL", "error", err, "email_encoded", emailEncoded) + shared.WriteError(w, http.StatusBadRequest, shared.ErrCodeBadRequest, "Invalid email format", nil) + return + } if docID == "" || email == "" { shared.WriteError(w, http.StatusBadRequest, shared.ErrCodeBadRequest, "Document ID and email are required", nil) @@ -263,8 +273,9 @@ func (h *Handler) HandleRemoveExpectedSigner(w http.ResponseWriter, r *http.Requ } // Remove expected signer - err := h.expectedSignerRepo.Remove(ctx, docID, email) + err = h.expectedSignerRepo.Remove(ctx, docID, email) if err != nil { + logger.Logger.Error("failed to remove expected signer", "error", err, "doc_id", docID, "email", email) shared.WriteError(w, http.StatusInternalServerError, shared.ErrCodeInternal, "Failed to remove expected signer", nil) return } @@ -598,7 +609,8 @@ func (h *Handler) HandleGetDocumentStatus(w http.ResponseWriter, r *http.Request // Get reminder stats if service available if h.reminderService != nil { - if reminderStats, err := h.reminderService.GetReminderStats(ctx, docID); err == nil { + reminderStats, err := h.reminderService.GetReminderStats(ctx, docID) + if err == nil && reminderStats != nil { var lastSentAt *string if reminderStats.LastSentAt != nil { formatted := reminderStats.LastSentAt.Format("2006-01-02T15:04:05Z07:00") @@ -609,6 +621,8 @@ func (h *Handler) HandleGetDocumentStatus(w http.ResponseWriter, r *http.Request PendingCount: reminderStats.PendingCount, LastSentAt: lastSentAt, } + } else if err != nil { + logger.Logger.Debug("Failed to get reminder stats", "doc_id", docID, "error", err.Error()) } } diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index c9a2251..51ac628 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -114,38 +114,29 @@ func NewServer(ctx context.Context, cfg *config.Config, frontend embed.FS, versi return nil, fmt.Errorf("failed to start webhook worker: %w", err) } - // Initialize Magic Link service (only if enabled) - MUST be before ReminderService - var magicLinkService *services.MagicLinkService - if cfg.Auth.MagicLinkEnabled && emailSender != nil { - magicLinkService = services.NewMagicLinkService(services.MagicLinkServiceConfig{ - Repository: magicLinkRepo, - EmailSender: emailSender, - BaseURL: cfg.App.BaseURL, - AppName: cfg.App.Organisation, - }) - logger.Logger.Info("Magic Link authentication enabled") - } else if !cfg.Auth.MagicLinkEnabled { - logger.Logger.Info("Magic Link authentication disabled") - } + magicLinkService := services.NewMagicLinkService(services.MagicLinkServiceConfig{ + Repository: magicLinkRepo, + EmailSender: emailSender, + BaseURL: cfg.App.BaseURL, + AppName: cfg.App.Organisation, + }) + logger.Logger.Info("Magic Link authentication enabled") // Initialize Magic Link cleanup worker var magicLinkWorker *workers.MagicLinkCleanupWorker - if magicLinkService != nil { + if cfg.Auth.MagicLinkEnabled { magicLinkWorker = workers.NewMagicLinkCleanupWorker(magicLinkService, 1*time.Hour) go magicLinkWorker.Start(ctx) } // Initialize reminder service with async support (needs magicLinkService) - var reminderService *services.ReminderAsyncService - if emailQueueRepo != nil && magicLinkService != nil { - reminderService = services.NewReminderAsyncService( - expectedSignerRepo, - reminderRepo, - emailQueueRepo, - magicLinkService, - cfg.App.BaseURL, - ) - } + reminderService := services.NewReminderAsyncService( + expectedSignerRepo, + reminderRepo, + emailQueueRepo, + magicLinkService, + cfg.App.BaseURL, + ) // Initialize OAuth session cleanup worker var sessionWorker *auth.SessionWorker @@ -191,7 +182,7 @@ func NewServer(ctx context.Context, cfg *config.Config, frontend embed.FS, versi router.Get("/oembed", handlers.HandleOEmbed(cfg.App.BaseURL)) - router.NotFound(EmbedFolder(frontend, "web/dist", cfg.App.BaseURL, version, cfg.Auth.OAuthEnabled, cfg.Auth.MagicLinkEnabled, cfg.App.OnlyAdminCanCreate, signatureRepo)) + router.NotFound(EmbedFolder(frontend, "web/dist", cfg.App.BaseURL, version, cfg.Auth.OAuthEnabled, cfg.Auth.MagicLinkEnabled, cfg.App.SMTPEnabled, cfg.App.OnlyAdminCanCreate, signatureRepo)) httpServer := &http.Server{ Addr: cfg.Server.ListenAddr, diff --git a/backend/pkg/web/static.go b/backend/pkg/web/static.go index b16c21b..c94f6a2 100644 --- a/backend/pkg/web/static.go +++ b/backend/pkg/web/static.go @@ -22,9 +22,10 @@ import ( // For index.html, it replaces __ACKIFY_BASE_URL__ placeholder with the actual base URL, // __ACKIFY_VERSION__ with the application version, // __ACKIFY_OAUTH_ENABLED__ and __ACKIFY_MAGICLINK_ENABLED__ with auth method flags, +// __ACKIFY_SMTP_ENABLED__ with SMTP availability flag, // __ACKIFY_ONLY_ADMIN_CAN_CREATE__ with document creation restriction flag, // and __META_TAGS__ with dynamic meta tags based on query parameters -func EmbedFolder(fsEmbed embed.FS, targetPath string, baseURL string, version string, oauthEnabled bool, magicLinkEnabled bool, onlyAdminCanCreate bool, signatureRepo *database.SignatureRepository) http.HandlerFunc { +func EmbedFolder(fsEmbed embed.FS, targetPath string, baseURL string, version string, oauthEnabled bool, magicLinkEnabled bool, smtpEnabled bool, onlyAdminCanCreate bool, signatureRepo *database.SignatureRepository) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fsys, err := fs.Sub(fsEmbed, targetPath) if err != nil { @@ -63,7 +64,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string, baseURL string, version st defer file.Close() if shouldServeIndex || strings.HasSuffix(cleanPath, "index.html") { - serveIndexTemplate(w, r, file, baseURL, version, oauthEnabled, magicLinkEnabled, onlyAdminCanCreate, signatureRepo) + serveIndexTemplate(w, r, file, baseURL, version, oauthEnabled, magicLinkEnabled, smtpEnabled, onlyAdminCanCreate, signatureRepo) return } @@ -72,7 +73,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string, baseURL string, version st } } -func serveIndexTemplate(w http.ResponseWriter, r *http.Request, file fs.File, baseURL string, version string, oauthEnabled bool, magicLinkEnabled bool, onlyAdminCanCreate bool, signatureRepo *database.SignatureRepository) { +func serveIndexTemplate(w http.ResponseWriter, r *http.Request, file fs.File, baseURL string, version string, oauthEnabled bool, magicLinkEnabled bool, smtpEnabled bool, onlyAdminCanCreate bool, signatureRepo *database.SignatureRepository) { content, err := io.ReadAll(file) if err != nil { logger.Logger.Error("Failed to read index.html", "error", err.Error()) @@ -92,6 +93,10 @@ func serveIndexTemplate(w http.ResponseWriter, r *http.Request, file fs.File, ba if magicLinkEnabled { magicLinkEnabledStr = "true" } + smtpEnabledStr := "false" + if smtpEnabled { + smtpEnabledStr = "true" + } onlyAdminCanCreateStr := "false" if onlyAdminCanCreate { onlyAdminCanCreateStr = "true" @@ -99,6 +104,7 @@ func serveIndexTemplate(w http.ResponseWriter, r *http.Request, file fs.File, ba processedContent = strings.ReplaceAll(processedContent, "__ACKIFY_OAUTH_ENABLED__", oauthEnabledStr) processedContent = strings.ReplaceAll(processedContent, "__ACKIFY_MAGICLINK_ENABLED__", magicLinkEnabledStr) + processedContent = strings.ReplaceAll(processedContent, "__ACKIFY_SMTP_ENABLED__", smtpEnabledStr) processedContent = strings.ReplaceAll(processedContent, "__ACKIFY_ONLY_ADMIN_CAN_CREATE__", onlyAdminCanCreateStr) metaTags := generateMetaTags(r, baseURL, signatureRepo) diff --git a/docs/en/configuration.md b/docs/en/configuration.md index 1f51ba0..611ff23 100644 --- a/docs/en/configuration.md +++ b/docs/en/configuration.md @@ -56,6 +56,28 @@ ACKIFY_OAUTH_LOGOUT_URL=https://your-provider.com/logout ACKIFY_OAUTH_SCOPES=openid,email,profile ``` +### Authentication Methods + +**Important**: At least ONE authentication method must be enabled (OAuth or MagicLink). + +```bash +# Force enable/disable OAuth (default: auto-detected from credentials) +ACKIFY_AUTH_OAUTH_ENABLED=true + +# Enable MagicLink passwordless authentication (default: false) +# Requires ACKIFY_MAIL_HOST to be configured +ACKIFY_AUTH_MAGICLINK_ENABLED=true +``` + +**Auto-detection**: +- **OAuth** is automatically enabled if `ACKIFY_OAUTH_CLIENT_ID` and `ACKIFY_OAUTH_CLIENT_SECRET` are set +- **MagicLink** requires explicit activation with `ACKIFY_AUTH_MAGICLINK_ENABLED=true` + SMTP configuration +- **SMTP/Email service** is automatically enabled when `ACKIFY_MAIL_HOST` is configured (independent of MagicLink) + +**Note**: SMTP and MagicLink are two distinct features: +- **SMTP** = Email reminder service for expected signers (auto-detected) +- **MagicLink** = Passwordless email authentication (requires explicit activation + SMTP) + ### Administration ```bash diff --git a/docs/fr/configuration.md b/docs/fr/configuration.md index ebf64e4..c31b505 100644 --- a/docs/fr/configuration.md +++ b/docs/fr/configuration.md @@ -56,6 +56,28 @@ ACKIFY_OAUTH_LOGOUT_URL=https://your-provider.com/logout ACKIFY_OAUTH_SCOPES=openid,email,profile ``` +### Méthodes d'Authentification + +**Important** : Au moins UNE méthode d'authentification doit être activée (OAuth ou MagicLink). + +```bash +# Forcer l'activation/désactivation d'OAuth (défaut: auto-détecté depuis les credentials) +ACKIFY_AUTH_OAUTH_ENABLED=true + +# Activer l'authentification MagicLink sans mot de passe (défaut: false) +# Nécessite que ACKIFY_MAIL_HOST soit configuré +ACKIFY_AUTH_MAGICLINK_ENABLED=true +``` + +**Auto-détection** : +- **OAuth** est automatiquement activé si `ACKIFY_OAUTH_CLIENT_ID` et `ACKIFY_OAUTH_CLIENT_SECRET` sont définis +- **MagicLink** nécessite une activation explicite avec `ACKIFY_AUTH_MAGICLINK_ENABLED=true` + configuration SMTP +- **Service SMTP/Email** est automatiquement activé quand `ACKIFY_MAIL_HOST` est configuré (indépendant de MagicLink) + +**Note** : SMTP et MagicLink sont deux fonctionnalités distinctes : +- **SMTP** = Service d'envoi de rappels email aux signataires attendus (auto-détecté) +- **MagicLink** = Authentification sans mot de passe par email (nécessite activation explicite + SMTP) + ### Administration ```bash diff --git a/webapp/index.html b/webapp/index.html index d070040..b676960 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -11,6 +11,7 @@ window.ACKIFY_VERSION = '__ACKIFY_VERSION__'; window.ACKIFY_OAUTH_ENABLED = '__ACKIFY_OAUTH_ENABLED__' === 'true'; window.ACKIFY_MAGICLINK_ENABLED = '__ACKIFY_MAGICLINK_ENABLED__' === 'true'; + window.ACKIFY_SMTP_ENABLED = '__ACKIFY_SMTP_ENABLED__' === 'true'; window.ACKIFY_ONLY_ADMIN_CAN_CREATE = '__ACKIFY_ONLY_ADMIN_CAN_CREATE__' === 'true'; diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b95af52..1be993d 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -370,6 +370,7 @@ "sendToAll": "An alle wartenden Leser senden ({count})", "sendToSelected": "Nur an Ausgewählte senden ({count})", "allContacted": "✓ Alle erwarteten Leser wurden kontaktiert oder haben bestätigt", + "emailServiceDisabled": "⚠️ Der E-Mail-Dienst ist derzeit deaktiviert. Der Verlauf der Erinnerungen bleibt sichtbar, aber das Senden neuer Erinnerungen ist nicht verfügbar.", "unexpectedSignatures": "⚠ Zusätzliche Lesebestätigungen", "unexpectedDescription": "Benutzer, die bestätigt haben, aber nicht in der Liste der erwarteten Leser stehen", "createdBy": "Erstellt von {by} am {date}", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 96611e3..44e752b 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -370,6 +370,7 @@ "sendToAll": "Send to all pending readers ({count})", "sendToSelected": "Send to selected only ({count})", "allContacted": "✓ All expected readers have been contacted or confirmed", + "emailServiceDisabled": "⚠️ The email service is currently disabled. Reminder history remains visible, but sending new reminders is not available.", "unexpectedSignatures": "⚠ Additional reading confirmations", "unexpectedDescription": "Users who confirmed but are not on the expected readers list", "createdBy": "Created by {by} on {date}", diff --git a/webapp/src/locales/es.json b/webapp/src/locales/es.json index 11daa4a..586e569 100644 --- a/webapp/src/locales/es.json +++ b/webapp/src/locales/es.json @@ -370,6 +370,7 @@ "sendToAll": "Enviar a todos los lectores en espera ({count})", "sendToSelected": "Enviar solo a los seleccionados ({count})", "allContacted": "✓ Todos los lectores esperados han sido contactados o han confirmado", + "emailServiceDisabled": "⚠️ El servicio de correo electrónico está actualmente desactivado. El historial de recordatorios permanece visible, pero el envío de nuevos recordatorios no está disponible.", "unexpectedSignatures": "⚠ Confirmaciones de lectura complementarias", "unexpectedDescription": "Usuarios que han confirmado pero no están presentes en la lista de lectores esperados", "createdBy": "Creado por {by} el {date}", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index af02634..e1bc0a7 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -367,6 +367,7 @@ "sendToAll": "Envoyer à tous les lecteurs en attente ({count})", "sendToSelected": "Envoyer uniquement aux sélectionnés ({count})", "allContacted": "✓ Tous les lecteurs attendus ont été contactés ou ont confirmé", + "emailServiceDisabled": "⚠️ Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", "unexpectedSignatures": "⚠ Confirmations de lecture complémentaires", "unexpectedDescription": "Utilisateurs ayant confirmé mais non présents dans la liste des lecteurs attendus", "createdBy": "Créé par {by} le {date}", diff --git a/webapp/src/locales/it.json b/webapp/src/locales/it.json index 3774b7e..0356b48 100644 --- a/webapp/src/locales/it.json +++ b/webapp/src/locales/it.json @@ -370,6 +370,7 @@ "sendToAll": "Invia a tutti i lettori in attesa ({count})", "sendToSelected": "Invia solo ai selezionati ({count})", "allContacted": "✓ Tutti i lettori previsti sono stati contattati o hanno confermato", + "emailServiceDisabled": "⚠️ Il servizio di posta elettronica è attualmente disabilitato. La cronologia dei promemoria rimane visibile, ma l'invio di nuovi promemoria non è disponibile.", "unexpectedSignatures": "⚠ Conferme di lettura aggiuntive", "unexpectedDescription": "Utenti che hanno confermato ma non presenti nell'elenco dei lettori previsti", "createdBy": "Creato da {by} il {date}", diff --git a/webapp/src/pages/admin/AdminDocumentDetail.vue b/webapp/src/pages/admin/AdminDocumentDetail.vue index fd768e2..5f37b4d 100644 --- a/webapp/src/pages/admin/AdminDocumentDetail.vue +++ b/webapp/src/pages/admin/AdminDocumentDetail.vue @@ -110,6 +110,7 @@ const shareLink = computed(() => { const stats = computed(() => documentStatus.value?.stats) const reminderStats = computed(() => documentStatus.value?.reminderStats) +const smtpEnabled = computed(() => (window as any).ACKIFY_SMTP_ENABLED || false) const expectedSigners = computed(() => documentStatus.value?.expectedSigners || []) const unexpectedSignatures = computed(() => documentStatus.value?.unexpectedSignatures || []) const documentMetadata = computed(() => documentStatus.value?.document) @@ -567,9 +568,10 @@ onMounted(() => { {{ signer.signedAt ? formatDate(signer.signedAt) : '-' }} - + - @@ -614,7 +616,7 @@ onMounted(() => { - + {{ t('admin.documentDetail.reminders') }} {{ t('admin.documentDetail.remindersDescription') }} @@ -636,8 +638,15 @@ onMounted(() => { - -
+ + + + {{ t('admin.documentDetail.emailServiceDisabled') }} + + + + +
-
+
{{ t('admin.documentDetail.allContacted') }}