diff --git a/app/lib/user-service.server.ts b/app/lib/user-service.server.ts index 58e8b949..de47d430 100644 --- a/app/lib/user-service.server.ts +++ b/app/lib/user-service.server.ts @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs' -import { and, eq, gt, isNull } from 'drizzle-orm' +import { and, eq, gt } from 'drizzle-orm' import ConfirmEmailAddress, { subject as ConfirmEmailAddressSubject, } from 'emails/confirm-email' @@ -42,7 +42,7 @@ import { actionToken, user, type User } from '~/schema' const ONE_HOUR_MILLIS: number = 60 * 60 * 1000 type RegisterUserResult = - | { ok: true; user: User } + | { ok: true; user: User; emailSent: boolean } | { ok: false field: 'username' | 'email' | 'password' | 'tos' | 'form' @@ -175,21 +175,28 @@ export const registerUser = async ( const token = await issueEmailConfirmationToken(newUser.id) - await sendMail({ - recipientAddress: newUser.email, - recipientName: newUser.name, - subject: NewUserEmailSubject[lng], - body: NewUserEmail({ - user: { name: newUser.name }, - email: newUser.email, - token, - language: lng, - }), - }) + let emailSent = true + try { + await sendMail({ + recipientAddress: newUser.email, + recipientName: newUser.name, + subject: NewUserEmailSubject[lng], + body: NewUserEmail({ + user: { name: newUser.name }, + email: newUser.email, + token, + language: lng, + }), + }) + } catch (err) { + console.error('Failed to send registration confirmation email:', err) + emailSent = false + } return { ok: true, user: newUser, + emailSent, } } diff --git a/app/routes/explore.forgot.tsx b/app/routes/explore.forgot.tsx index dbfa9a27..642ae146 100644 --- a/app/routes/explore.forgot.tsx +++ b/app/routes/explore.forgot.tsx @@ -61,7 +61,7 @@ export async function action({ request }: ActionFunctionArgs) { console.warn(err) return data( { - errors: { email: 'An error occurred. Please try again later.' }, + errors: { email: 'generic_error_try_again' }, success: false, }, { status: 500 }, diff --git a/app/routes/explore.register.tsx b/app/routes/explore.register.tsx index 66d1e1ee..be237d91 100644 --- a/app/routes/explore.register.tsx +++ b/app/routes/explore.register.tsx @@ -191,14 +191,18 @@ export async function action({ request }: ActionFunctionArgs) { }, { status: 400 }, ) -} + } + + if (!result.emailSent) { + return data({ emailDeliveryFailed: true }, { status: 200 }) + } return createUserSession({ - request, - userId: result.user.id, - remember: false, - redirectTo, -}) + request, + userId: result.user.id, + remember: false, + redirectTo, + }) } export const meta: MetaFunction = () => { @@ -215,15 +219,45 @@ export default function RegisterDialog() { const passwordRef = React.useRef(null) React.useEffect(() => { - if (actionData?.errors?.username) { - usernameRef.current?.focus() - } else if (actionData?.errors?.email) { - emailRef.current?.focus() - } else if (actionData?.errors?.password) { - passwordRef.current?.focus() + if (actionData && 'errors' in actionData) { + if (actionData.errors?.username) { + usernameRef.current?.focus() + } else if (actionData.errors?.email) { + emailRef.current?.focus() + } else if (actionData.errors?.password) { + passwordRef.current?.focus() + } } }, [actionData]) + const actionErrors = actionData && 'errors' in actionData ? actionData.errors : undefined + + if (actionData && 'emailDeliveryFailed' in actionData && actionData.emailDeliveryFailed) { + return ( +
+ +
+ + + + {t('account_created')} + {t('email_delivery_failed_description')} + + + + + + + +
+ ) + } + return (
{t('username_hint')}

- {actionData?.errors?.username && ( + {actionErrors?.username && (
- {t(actionData.errors.username)} + {t(actionErrors?.username)}
)}
@@ -278,12 +312,12 @@ export default function RegisterDialog() { autoFocus={true} name="email" autoComplete="email" - aria-invalid={actionData?.errors?.email ? true : undefined} + aria-invalid={actionErrors?.email ? true : undefined} aria-describedby="email-error" /> - {actionData?.errors?.email && ( + {actionErrors?.email && (
- {t(actionData.errors.email)} + {t(actionErrors?.email)}
)}
@@ -296,15 +330,15 @@ export default function RegisterDialog() { ref={passwordRef} name="password" autoComplete="new-password" - aria-invalid={actionData?.errors?.password ? true : undefined} + aria-invalid={actionErrors?.password ? true : undefined} aria-describedby="password-error" />

{t('password_hint')}

- {actionData?.errors?.password && ( + {actionErrors?.password && (
- {t(actionData.errors.password)} + {t(actionErrors?.password)}
)} @@ -314,7 +348,7 @@ export default function RegisterDialog() { name="tosAccepted" type="checkbox" className="mt-1 h-4 w-4" - aria-invalid={actionData?.errors?.tosAccepted ? true : undefined} + aria-invalid={actionErrors?.tosAccepted ? true : undefined} aria-describedby="tos-error" /> - {actionData?.errors?.tosAccepted && ( + {actionErrors?.tosAccepted && (
- {t(actionData.errors.tosAccepted)} + {t(actionErrors?.tosAccepted)}
)} diff --git a/app/routes/settings.account.tsx b/app/routes/settings.account.tsx index c87f0b9f..fc6cd22c 100644 --- a/app/routes/settings.account.tsx +++ b/app/routes/settings.account.tsx @@ -164,13 +164,26 @@ export async function action({ request }: ActionFunctionArgs) { if (wantsEmailChange) { const [updatedUser] = await updateUserEmail(user, email) - await resendEmailConfirmation(updatedUser) + try { + await resendEmailConfirmation(updatedUser) + } catch (err) { + console.error('Failed to send email confirmation after email change:', err) + return data( + { + intent: 'update-profile', + errors: { name: null, email: null, passwordUpdate: null }, + emailDeliveryFailed: true, + }, + { status: 200 }, + ) + } } return data( { intent: 'update-profile', errors: { name: null, email: null, passwordUpdate: null }, + emailDeliveryFailed: false, }, { status: 200 }, ) @@ -231,6 +244,11 @@ export default function EditUserProfilePage() { return } + if ('emailDeliveryFailed' in actionData && actionData.emailDeliveryFailed) { + toast({ title: t('email_change_delivery_failed'), variant: 'destructive' }) + return + } + toast({ title: t('profile_successfully_updated'), variant: 'success' }) }, [actionData, toast, t]) diff --git a/public/locales/de/forgot-password.json b/public/locales/de/forgot-password.json index bb1aa403..97450252 100644 --- a/public/locales/de/forgot-password.json +++ b/public/locales/de/forgot-password.json @@ -15,5 +15,5 @@ "back_to_login": "Zurück zur Anmeldung", "Email is invalid": "Ungültige E-Mail-Adresse", - "generic_error_try_again": "Ein Fehler ist aufgetreten. Bitte versuche es später erneut." + "generic_error_try_again": "Wir konnten die E-Mail zum Zurücksetzen nicht senden. Bitte versuche es später erneut. Wenn dieses Problem weiterhin besteht, kannst du einen Fehlerbericht einreichen." } \ No newline at end of file diff --git a/public/locales/de/register.json b/public/locales/de/register.json index 6f8a1603..39ba5804 100644 --- a/public/locales/de/register.json +++ b/public/locales/de/register.json @@ -25,5 +25,9 @@ "agree_tos_prefix": "Ich stimme den", "terms_of_service": "Nutzungsbedingungen", "agree_tos_suffix": "zu.", - "tos_must_accept": "Den Nutzungsbedingungen muss zugestimmt werden." + "tos_must_accept": "Den Nutzungsbedingungen muss zugestimmt werden.", + + "account_created": "Konto erstellt", + "email_delivery_failed_description": "Dein Konto wurde erfolgreich erstellt, aber wir konnten keine Bestätigungs-E-Mail senden. Bitte logge dich ein und sende sie erneut über deine Kontoeinstellungen. Wenn dieses Problem weiterhin besteht, kannst du einen Fehlerbericht einreichen.", + "go_to_login": "Zur Anmeldung" } diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index d657d1df..55adce3a 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -39,6 +39,8 @@ "sending": "Wird gesendet...", "verification_email_sent": "Bestätigungs-E-Mail gesendet", "verification_email_failed": "Bestätigungs-E-Mail konnte nicht gesendet werden.", + "email_change_delivery_failed": "E-Mail-Adresse aktualisiert, aber wir konnten keine Bestätigungs-E-Mail senden. Bitte nutze die Schaltfläche zum erneuten Senden.", + "verification_link_invalid": "Der Bestätigungslink ist ungültig oder abgelaufen.", "email_already_confirmed": "E-Mail ist bereits bestätigt.", "language": "Sprache", "select_language": "Wähle die Sprache aus", diff --git a/public/locales/en/forgot-password.json b/public/locales/en/forgot-password.json index f920164d..d5bdacaa 100644 --- a/public/locales/en/forgot-password.json +++ b/public/locales/en/forgot-password.json @@ -15,5 +15,5 @@ "back_to_login": "Back to Login", "Email is invalid": "Email is invalid", - "generic_error_try_again": "An error occurred. Please try again later." + "generic_error_try_again": "We couldn't send the reset email. Please try again later. If this issue persists, consider opening a bug report." } \ No newline at end of file diff --git a/public/locales/en/register.json b/public/locales/en/register.json index 0e38281d..721f77c2 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -25,5 +25,9 @@ "agree_tos_prefix": "I agree to the", "terms_of_service": "Terms of Service", "agree_tos_suffix": ".", - "tos_must_accept": "The terms of service must be accepted." + "tos_must_accept": "The terms of service must be accepted.", + + "account_created": "Account Created", + "email_delivery_failed_description": "Your account was successfully created, but we couldn't send the confirmation email. Please log in and resend it from your account settings. If this issue persists, consider opening a bug report.", + "go_to_login": "Go to Login" } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index cb6e9569..1c9932e6 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -42,6 +42,8 @@ "sending": "Sending...", "verification_email_sent": "Verification email sent", "verification_email_failed": "Failed to send verification email", + "email_change_delivery_failed": "Email address updated, but we couldn't send the confirmation email. Please use the resend button to try again.", + "verification_link_invalid": "Verification link is invalid or has expired.", "email_already_confirmed": "Email is already confirmed", "language": "Language", "select_language": "Select language",