Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/client/src/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions apps/client/src/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,6 +37,7 @@ export const EmailCallbackForm: Component<{
}> = (props) => {
const { t } = useI18n();
const { config } = useConfig();
const [getShowUpdateWebhookSecretForm, setShowUpdateWebhookSecretForm] = createSignal(false);

const { availableDomains } = config.emailCallbacks;

Expand Down Expand Up @@ -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 (
<Form class="flex flex-col gap-4">

Expand Down Expand Up @@ -247,29 +260,59 @@ export const EmailCallbackForm: Component<{
<TextFieldLabel for="webhookSecret" class="flex items-baseline gap-2">
{t('email-callbacks.form.webhook.secret.label')}

<span class="bg-background text-xs leading-tight px-1.5 py-0.5 rounded border text-muted-foreground">
{t('email-callbacks.form.recommended')}
</span>
<Show when={!props.emailCallback?.hasWebhookSecret}>
<span class="bg-background text-xs leading-tight px-1.5 py-0.5 rounded border text-muted-foreground">
{t('email-callbacks.form.recommended')}
</span>
</Show>
</TextFieldLabel>

<div class="flex items-center gap-2">
<TextField
type="text"
id="webhookSecret"
placeholder={t('email-callbacks.form.webhook.secret.placeholder')}
{...inputProps}
value={field.value}
aria-invalid={Boolean(field.error)}
/>
<Button
class="flex-shrink-0"
variant="outline"
size="icon"
onClick={() => setValue(form, 'webhookSecret', generateEmailCallbackSecret())}
>
<div class="i-tabler-refresh size-4"></div>
</Button>
</div>
<Show
when={getShowWebhookSecretForm()}
fallback={(
<>
<div class="flex items-center gap-2">
<TextField
type="text"
id="webhookSecret"
value="**********************"
disabled
/>
<Button
class="flex-shrink-0"
variant="outline"
onClick={() => setShowUpdateWebhookSecretForm(true)}
>
{t('email-callbacks.form.webhook.secret.rotate')}
</Button>
</div>
<p class="text-sm text-muted-foreground">
{t('email-callbacks.form.webhook.secret.cannot-read-secret')}
</p>
</>
)}
>
<div class="flex items-center gap-2">
<TextField
type="text"
id="webhookSecret"
placeholder={t('email-callbacks.form.webhook.secret.placeholder')}
{...inputProps}
value={field.value}
aria-invalid={Boolean(field.error)}
autofocus
/>
<Button
class="flex-shrink-0"
variant="outline"
size="icon"
onClick={() => setValue(form, 'webhookSecret', generateEmailCallbackSecret())}
>
<div class="i-tabler-refresh size-4"></div>
</Button>
</div>
</Show>

{field.error && <div class="text-red-500 text-sm">{field.error}</div>}
</TextFieldRoot>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type EmailCallback = {
username: string;
allowedOrigins: string[];
webhookUrl: string;
webhookSecret?: string;
hasWebhookSecret: boolean;
};

export type EmailProcessing = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/modules/i18n/locales.types.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
};
}

Expand Down