diff --git a/apps/client/src/locales/en.yml b/apps/client/src/locales/en.yml index cdc3b77..66efcec 100644 --- a/apps/client/src/locales/en.yml +++ b/apps/client/src/locales/en.yml @@ -59,6 +59,8 @@ email-callbacks: label: Webhook secret placeholder: eg. 1234567890 random-secret: Random secret + rotate: Rotate secret + cannot-read-secret: For security reasons, the secret cannot be read, you can change it by clicking the rotate button. allowed-origins: title: Allowed email origins description: Configure the addresses that are allowed to send emails to your email address, leave empty to allow all. diff --git a/apps/client/src/locales/fr.yml b/apps/client/src/locales/fr.yml index 7dfcdc4..35fdcdf 100644 --- a/apps/client/src/locales/fr.yml +++ b/apps/client/src/locales/fr.yml @@ -43,6 +43,19 @@ email-callbacks: description: Choisissez un nom d'utilisateur et un domaine pour votre adresse email. random-address: Adresse aléatoire placeholder: ex. john.doe + webhook: + title: Webhook + description: Configurez votre webhook pour recevoir les emails envoyés à votre adresse email. + url: + label: URL du webhook + placeholder: ex. https://example.com/callback + random-url: URL aléatoire + secret: + label: Secret du webhook + placeholder: ex. 1234567890 + random-secret: Secret aléatoire + rotate: Régénérer le secret + cannot-read-secret: Pour des raisons de sécurité, le secret ne peut pas être lu, vous pouvez le modifier en cliquant sur le bouton de rotation. allowed-origins: title: Origines autorisées description: Configurez les adresses qui sont autorisées à envoyer des emails à votre adresse email, laissez vide pour autoriser toutes les adresses. diff --git a/apps/client/src/modules/email-callbacks/components/email-callback-form.component.tsx b/apps/client/src/modules/email-callbacks/components/email-callback-form.component.tsx index 357703e..3558ac3 100644 --- a/apps/client/src/modules/email-callbacks/components/email-callback-form.component.tsx +++ b/apps/client/src/modules/email-callbacks/components/email-callback-form.component.tsx @@ -10,7 +10,7 @@ import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/component import { safely } from '@corentinth/chisels'; import { generateId } from '@corentinth/friendly-ids'; import { type FormStore, insert, remove, reset, setValue } from '@modular-forms/solid'; -import { type Component, For, type JSX } from 'solid-js'; +import { type Component, createSignal, For, type JSX, Show } from 'solid-js'; import * as v from 'valibot'; import { emailUsernameRegex } from '../email-callbacks.constants'; import { generateEmailCallbackSecret } from '../email-callbacks.models'; @@ -37,6 +37,7 @@ export const EmailCallbackForm: Component<{ }> = (props) => { const { t } = useI18n(); const { config } = useConfig(); + const [getShowUpdateWebhookSecretForm, setShowUpdateWebhookSecretForm] = createSignal(false); const { availableDomains } = config.emailCallbacks; @@ -120,6 +121,18 @@ export const EmailCallbackForm: Component<{ }, }); + const getShowWebhookSecretForm = () => { + if (props.emailCallback === undefined) { + return true; + } + + if (props.emailCallback.hasWebhookSecret) { + return getShowUpdateWebhookSecretForm(); + } + + return false; + }; + return (
@@ -247,29 +260,59 @@ export const EmailCallbackForm: Component<{ {t('email-callbacks.form.webhook.secret.label')} - - {t('email-callbacks.form.recommended')} - + + + {t('email-callbacks.form.recommended')} + + -
- - -
+ +
+ + +
+

+ {t('email-callbacks.form.webhook.secret.cannot-read-secret')} +

+ + )} + > +
+ + +
+
+ {field.error &&
{field.error}
} )} diff --git a/apps/client/src/modules/email-callbacks/email-callbacks.types.ts b/apps/client/src/modules/email-callbacks/email-callbacks.types.ts index 2c0e8f3..8fe7c46 100644 --- a/apps/client/src/modules/email-callbacks/email-callbacks.types.ts +++ b/apps/client/src/modules/email-callbacks/email-callbacks.types.ts @@ -7,7 +7,7 @@ export type EmailCallback = { username: string; allowedOrigins: string[]; webhookUrl: string; - webhookSecret?: string; + hasWebhookSecret: boolean; }; export type EmailProcessing = { diff --git a/apps/client/src/modules/email-callbacks/pages/email-callback-settings.page.tsx b/apps/client/src/modules/email-callbacks/pages/email-callback-settings.page.tsx index 67ea0c8..9e3b7c8 100644 --- a/apps/client/src/modules/email-callbacks/pages/email-callback-settings.page.tsx +++ b/apps/client/src/modules/email-callbacks/pages/email-callback-settings.page.tsx @@ -15,10 +15,7 @@ export const EmailCallbackSettingsPage: Component = () => { const handleUpdateEmailCallback = async (args: EmailCallbackFormResult) => { await updateEmailCallback({ emailCallbackId: emailCallback.id, - emailCallback: { - ...args, - webhookSecret: args.webhookSecret !== emailCallback.webhookSecret ? args.webhookSecret : undefined, - }, + emailCallback: args, }); createToast({ diff --git a/apps/client/src/modules/i18n/locales.types.ts b/apps/client/src/modules/i18n/locales.types.ts index 375dd95..4dc5d42 100644 --- a/apps/client/src/modules/i18n/locales.types.ts +++ b/apps/client/src/modules/i18n/locales.types.ts @@ -1,2 +1,2 @@ // Dynamically generated file. Use "pnpm script:generate-i18n-types" to update. -export type LocaleKeys = 'auth.login.title' | 'auth.login.description' | 'auth.login.login-with-provider' | 'auth.login.no-account' | 'auth.login.register' | 'auth.email-validation-required.title' | 'auth.email-validation-required.description' | 'auth.legal-links.description' | 'auth.legal-links.terms' | 'auth.legal-links.privacy' | 'layout.account-settings' | 'layout.upgrade-to-pro' | 'layout.language' | 'layout.api-keys' | 'email-callbacks.settings' | 'email-callbacks.inbox' | 'email-callbacks.back-to-emails' | 'email-callbacks.copy-email-address.label' | 'email-callbacks.copy-email-address.copied' | 'email-callbacks.enable' | 'email-callbacks.disable' | 'email-callbacks.delete' | 'email-callbacks.disabled.label' | 'email-callbacks.disabled.tooltip' | 'email-callbacks.list.create-email' | 'email-callbacks.list.empty' | 'email-callbacks.list.your-emails' | 'email-callbacks.list.view-history' | 'email-callbacks.form.title' | 'email-callbacks.form.recommended' | 'email-callbacks.form.address.title' | 'email-callbacks.form.address.description' | 'email-callbacks.form.address.random-address' | 'email-callbacks.form.address.placeholder' | 'email-callbacks.form.webhook.title' | 'email-callbacks.form.webhook.description' | 'email-callbacks.form.webhook.url.label' | 'email-callbacks.form.webhook.url.placeholder' | 'email-callbacks.form.webhook.url.random-url' | 'email-callbacks.form.webhook.secret.label' | 'email-callbacks.form.webhook.secret.placeholder' | 'email-callbacks.form.webhook.secret.random-secret' | 'email-callbacks.form.allowed-origins.title' | 'email-callbacks.form.allowed-origins.description' | 'email-callbacks.form.allowed-origins.placeholder' | 'email-callbacks.form.allowed-origins.add-email' | 'email-callbacks.form.allowed-origins.remove-email' | 'email-callbacks.form.create-email' | 'email-callbacks.form.update.save-changes' | 'email-callbacks.form.update.changes-saved' | 'email-callbacks.form.update.toast' | 'email-callbacks.form.validation.username.required' | 'email-callbacks.form.validation.username.invalid' | 'email-callbacks.form.validation.username.min-length' | 'email-callbacks.form.validation.username.invalid-characters' | 'email-callbacks.form.validation.domain.invalid' | 'email-callbacks.form.validation.webhook-url.required' | 'email-callbacks.form.validation.webhook-url.invalid' | 'email-callbacks.form.validation.webhook-secret.min-length' | 'email-callbacks.form.validation.allowed-origins.invalid' | 'email-callbacks.form.validation.already-exists' | 'email-callbacks.form.validation.limit-reached' | 'email-callbacks.form.validation.unknown' | 'email-callbacks.created.title' | 'email-callbacks.created.description' | 'email-callbacks.created.back-to-emails' | 'email-callbacks.created.copy-email-address' | 'processing.status.success' | 'processing.status.error' | 'processing.status.not-processed' | 'processing.empty.title' | 'processing.empty.description' | 'processing.error.from-address-not-allowed' | 'processing.error.webhook-failed' | 'processing.error.not-enabled' | 'processing.error.unknown' | 'tables.rows-per-page' | 'tables.page-description' | 'theme.light' | 'theme.dark' | 'theme.system' | 'api-keys.create-api-key' | 'api-keys.list.title' | 'api-keys.list.description' | 'api-keys.empty.title' | 'api-keys.empty.description' | 'api-keys.create.title' | 'api-keys.create.submit' | 'api-keys.create.name.required' | 'api-keys.create.name.max-length' | 'api-keys.create.name.label' | 'api-keys.create.name.placeholder' | 'api-keys.create.name.description' | 'api-keys.create.success.title' | 'api-keys.create.success.description' | 'api-keys.delete.success' | 'api-keys.delete.confirm.title' | 'api-keys.delete.confirm.description' | 'api-keys.delete.confirm.confirm-button' | 'api-keys.delete.confirm.cancel-button'; +export type LocaleKeys = 'auth.login.title' | 'auth.login.description' | 'auth.login.login-with-provider' | 'auth.login.no-account' | 'auth.login.register' | 'auth.email-validation-required.title' | 'auth.email-validation-required.description' | 'auth.legal-links.description' | 'auth.legal-links.terms' | 'auth.legal-links.privacy' | 'layout.account-settings' | 'layout.upgrade-to-pro' | 'layout.language' | 'layout.api-keys' | 'email-callbacks.settings' | 'email-callbacks.inbox' | 'email-callbacks.back-to-emails' | 'email-callbacks.copy-email-address.label' | 'email-callbacks.copy-email-address.copied' | 'email-callbacks.enable' | 'email-callbacks.disable' | 'email-callbacks.delete' | 'email-callbacks.disabled.label' | 'email-callbacks.disabled.tooltip' | 'email-callbacks.list.create-email' | 'email-callbacks.list.empty' | 'email-callbacks.list.your-emails' | 'email-callbacks.list.view-history' | 'email-callbacks.form.title' | 'email-callbacks.form.recommended' | 'email-callbacks.form.address.title' | 'email-callbacks.form.address.description' | 'email-callbacks.form.address.random-address' | 'email-callbacks.form.address.placeholder' | 'email-callbacks.form.webhook.title' | 'email-callbacks.form.webhook.description' | 'email-callbacks.form.webhook.url.label' | 'email-callbacks.form.webhook.url.placeholder' | 'email-callbacks.form.webhook.url.random-url' | 'email-callbacks.form.webhook.secret.label' | 'email-callbacks.form.webhook.secret.placeholder' | 'email-callbacks.form.webhook.secret.random-secret' | 'email-callbacks.form.webhook.secret.rotate' | 'email-callbacks.form.webhook.secret.cannot-read-secret' | 'email-callbacks.form.allowed-origins.title' | 'email-callbacks.form.allowed-origins.description' | 'email-callbacks.form.allowed-origins.placeholder' | 'email-callbacks.form.allowed-origins.add-email' | 'email-callbacks.form.allowed-origins.remove-email' | 'email-callbacks.form.create-email' | 'email-callbacks.form.update.save-changes' | 'email-callbacks.form.update.changes-saved' | 'email-callbacks.form.update.toast' | 'email-callbacks.form.validation.username.required' | 'email-callbacks.form.validation.username.invalid' | 'email-callbacks.form.validation.username.min-length' | 'email-callbacks.form.validation.username.invalid-characters' | 'email-callbacks.form.validation.domain.invalid' | 'email-callbacks.form.validation.webhook-url.required' | 'email-callbacks.form.validation.webhook-url.invalid' | 'email-callbacks.form.validation.webhook-secret.min-length' | 'email-callbacks.form.validation.allowed-origins.invalid' | 'email-callbacks.form.validation.already-exists' | 'email-callbacks.form.validation.limit-reached' | 'email-callbacks.form.validation.unknown' | 'email-callbacks.created.title' | 'email-callbacks.created.description' | 'email-callbacks.created.back-to-emails' | 'email-callbacks.created.copy-email-address' | 'processing.status.success' | 'processing.status.error' | 'processing.status.not-processed' | 'processing.empty.title' | 'processing.empty.description' | 'processing.error.from-address-not-allowed' | 'processing.error.webhook-failed' | 'processing.error.not-enabled' | 'processing.error.unknown' | 'tables.rows-per-page' | 'tables.page-description' | 'theme.light' | 'theme.dark' | 'theme.system' | 'api-keys.create-api-key' | 'api-keys.list.title' | 'api-keys.list.description' | 'api-keys.empty.title' | 'api-keys.empty.description' | 'api-keys.create.title' | 'api-keys.create.submit' | 'api-keys.create.name.required' | 'api-keys.create.name.max-length' | 'api-keys.create.name.label' | 'api-keys.create.name.placeholder' | 'api-keys.create.name.description' | 'api-keys.create.success.title' | 'api-keys.create.success.description' | 'api-keys.delete.success' | 'api-keys.delete.confirm.title' | 'api-keys.delete.confirm.description' | 'api-keys.delete.confirm.confirm-button' | 'api-keys.delete.confirm.cancel-button'; diff --git a/apps/server/src/modules/email-callbacks/email-callbacks.models.test.ts b/apps/server/src/modules/email-callbacks/email-callbacks.models.test.ts index 1545d02..f486226 100644 --- a/apps/server/src/modules/email-callbacks/email-callbacks.models.test.ts +++ b/apps/server/src/modules/email-callbacks/email-callbacks.models.test.ts @@ -23,7 +23,7 @@ describe('email-callbacks models', () => { username: 'test', domain: 'test.com', webhookUrl: 'https://example.com/webhook', - webhookSecret: '************************', + hasWebhookSecret: true, allowedOrigins: [], isEnabled: true, createdAt: new Date('2025-01-01'), @@ -33,25 +33,25 @@ describe('email-callbacks models', () => { }); test('the webhook secret is not redacted if it is not set', () => { - const emailCallback: EmailCallback = { - id: '1', - username: 'test', - domain: 'test.com', - webhookUrl: 'https://example.com/webhook', - webhookSecret: null, - allowedOrigins: [], - isEnabled: true, - createdAt: new Date('2025-01-01'), - updatedAt: new Date('2025-01-01'), - userId: '1', - }; - - expect(formatEmailCallbackForApi({ emailCallback })).to.deep.equal({ + expect( + formatEmailCallbackForApi({ emailCallback: { + id: '1', + username: 'test', + domain: 'test.com', + webhookUrl: 'https://example.com/webhook', + webhookSecret: null, + allowedOrigins: [], + isEnabled: true, + createdAt: new Date('2025-01-01'), + updatedAt: new Date('2025-01-01'), + userId: '1', + } }), + ).to.deep.equal({ id: '1', username: 'test', domain: 'test.com', webhookUrl: 'https://example.com/webhook', - webhookSecret: undefined, + hasWebhookSecret: false, allowedOrigins: [], isEnabled: true, createdAt: new Date('2025-01-01'), diff --git a/apps/server/src/modules/email-callbacks/email-callbacks.models.ts b/apps/server/src/modules/email-callbacks/email-callbacks.models.ts index 8a92f4c..f5566ae 100644 --- a/apps/server/src/modules/email-callbacks/email-callbacks.models.ts +++ b/apps/server/src/modules/email-callbacks/email-callbacks.models.ts @@ -1,10 +1,13 @@ import type { Address } from 'postal-mime'; import type { EmailCallback } from './email-callbacks.types'; +import { isNil } from 'lodash-es'; export function formatEmailCallbackForApi({ emailCallback }: { emailCallback: EmailCallback }) { + const { webhookSecret, ...rest } = emailCallback; + return { - ...emailCallback, - webhookSecret: emailCallback.webhookSecret ? '************************' : undefined, + ...rest, + hasWebhookSecret: !isNil(webhookSecret), }; }