Skip to content
Open
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
33 changes: 20 additions & 13 deletions app/lib/user-service.server.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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,
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/routes/explore.forgot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
80 changes: 57 additions & 23 deletions app/routes/explore.register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -215,15 +219,45 @@ export default function RegisterDialog() {
const passwordRef = React.useRef<HTMLInputElement>(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 (
<div className="flex h-screen items-center justify-center">
<Link
to={{
pathname: '/explore',
search: searchParams.toString(),
}}
>
<div className="fixed inset-0 z-40 h-full w-full bg-black opacity-25" />
</Link>
<Card className="z-50 w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold">{t('account_created')}</CardTitle>
<CardDescription>{t('email_delivery_failed_description')}</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-center gap-2">
<Link to="/explore/login" className="w-full">
<Button className="w-full bg-light-blue">{t('go_to_login')}</Button>
</Link>
</CardFooter>
</Card>
</div>
)
}

return (
<div className="flex h-screen items-center justify-center">
<Link
Expand Down Expand Up @@ -261,9 +295,9 @@ export default function RegisterDialog() {
<p className="text-xs text-muted-foreground">
{t('username_hint')}
</p>
{actionData?.errors?.username && (
{actionErrors?.username && (
<div className="mt-1 text-sm text-red-500" id="password-error">
{t(actionData.errors.username)}
{t(actionErrors?.username)}
</div>
)}
</div>
Expand All @@ -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 && (
<div className="mt-1 text-sm text-red-500" id="email-error">
{t(actionData.errors.email)}
{t(actionErrors?.email)}
</div>
)}
</div>
Expand All @@ -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"
/>
<p className="text-xs text-muted-foreground">
{t('password_hint')}
</p>
{actionData?.errors?.password && (
{actionErrors?.password && (
<div className="mt-1 text-sm text-red-500" id="password-error">
{t(actionData.errors.password)}
{t(actionErrors?.password)}
</div>
)}
</div>
Expand All @@ -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"
/>
<Label htmlFor="tosAccepted" className="text-sm leading-5">
Expand All @@ -325,9 +359,9 @@ export default function RegisterDialog() {
{t('agree_tos_suffix')}
</Label>
</div>
{actionData?.errors?.tosAccepted && (
{actionErrors?.tosAccepted && (
<div className="mt-1 text-sm text-red-500" id="tos-error">
{t(actionData.errors.tosAccepted)}
{t(actionErrors?.tosAccepted)}
</div>
)}
</CardContent>
Expand Down
20 changes: 19 additions & 1 deletion app/routes/settings.account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
)
Expand Down Expand Up @@ -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])

Expand Down
2 changes: 1 addition & 1 deletion public/locales/de/forgot-password.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
6 changes: 5 additions & 1 deletion public/locales/de/register.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 2 additions & 0 deletions public/locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion public/locales/en/forgot-password.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
6 changes: 5 additions & 1 deletion public/locales/en/register.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 2 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading