diff --git a/packages/app/prisma/migrations/20231120113500_init/migration.sql b/packages/app/prisma/migrations/20231210183722_init/migration.sql similarity index 99% rename from packages/app/prisma/migrations/20231120113500_init/migration.sql rename to packages/app/prisma/migrations/20231210183722_init/migration.sql index e70ca034..6de2c75b 100644 --- a/packages/app/prisma/migrations/20231120113500_init/migration.sql +++ b/packages/app/prisma/migrations/20231210183722_init/migration.sql @@ -27,6 +27,7 @@ CREATE TABLE "User" ( "role" TEXT NOT NULL DEFAULT 'user', "password" TEXT, "hasPassword" BOOLEAN NOT NULL DEFAULT false, + "lastLocale" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, diff --git a/packages/app/prisma/migrations/20231120113500_sp/migration.sql b/packages/app/prisma/migrations/20231210183723_sp/migration.sql similarity index 100% rename from packages/app/prisma/migrations/20231120113500_sp/migration.sql rename to packages/app/prisma/migrations/20231210183723_sp/migration.sql diff --git a/packages/app/prisma/schema.prisma b/packages/app/prisma/schema.prisma index fb6d47b4..6233542a 100644 --- a/packages/app/prisma/schema.prisma +++ b/packages/app/prisma/schema.prisma @@ -45,6 +45,7 @@ model User { hasPassword Boolean @default(false) resetPasswordToken ResetPassordToken? userEmailVerificationToken UserEmailVerificationToken? + lastLocale String? // Timestamps createdAt DateTime @default(now()) diff --git a/packages/app/src/api/auth/mutations.ts b/packages/app/src/api/auth/mutations.ts index 9cb15ff2..6d1f8ed9 100644 --- a/packages/app/src/api/auth/mutations.ts +++ b/packages/app/src/api/auth/mutations.ts @@ -5,6 +5,7 @@ import { hash } from "@/lib/bcrypt" import { logger } from "@/lib/logger" import { sendMail } from "@/lib/mailer" import { prisma } from "@/lib/prisma" +import { redis } from "@/lib/redis" import { signUpSchema } from "@/lib/schemas/auth" import { html, plainText, subject } from "@/lib/templates/mail/verify-email" import { ApiError, handleApiError, throwableErrorsMessages } from "@/lib/utils/server-utils" @@ -25,8 +26,10 @@ export const register = async ({ input }: apiInputFromSchema`, to: email.toLowerCase(), subject: subject, - text: plainText(url), - html: html(url), + text: plainText(url, input.locale), + html: html(url, input.locale), }) } else { logger.debug("Email verification disabled, skipping email sending on registration") diff --git a/packages/app/src/api/me/email/mutation.ts b/packages/app/src/api/me/email/mutation.ts index 5b5acbb2..816b770f 100644 --- a/packages/app/src/api/me/email/mutation.ts +++ b/packages/app/src/api/me/email/mutation.ts @@ -1,5 +1,6 @@ import { randomUUID } from "crypto" import { env } from "env.mjs" +import { i18n } from "i18n-config" import { logger } from "@/lib/logger" import { sendMail } from "@/lib/mailer" @@ -20,6 +21,7 @@ export const sendVerificationEmail = async ({ input }: apiInputFromSchema`, to: email.toLowerCase(), subject: subject, - text: plainText(url), - html: html(url), + text: plainText(url, user.lastLocale ?? i18n.defaultLocale), + html: html(url, user.lastLocale ?? i18n.defaultLocale), }) } else { logger.debug("Email verification disabled") diff --git a/packages/app/src/api/me/password/mutation.ts b/packages/app/src/api/me/password/mutation.ts index 240de546..5160dffb 100644 --- a/packages/app/src/api/me/password/mutation.ts +++ b/packages/app/src/api/me/password/mutation.ts @@ -1,5 +1,6 @@ import { randomUUID } from "crypto" import { env } from "env.mjs" +import { i18n } from "i18n-config" import { hash } from "@/lib/bcrypt" import { logger } from "@/lib/logger" @@ -63,8 +64,16 @@ export const forgotPassword = async ({ input }: apiInputFromSchema`, to: email, subject: subject, - text: plainText(user.username ?? email, `${env.VERCEL_URL ?? env.BASE_URL}/reset-password/${resetPasswordToken}`), - html: html(user.username ?? email, `${env.VERCEL_URL ?? env.BASE_URL}/reset-password/${resetPasswordToken}`), + text: plainText( + user.username ?? email, + `${env.VERCEL_URL ?? env.BASE_URL}/reset-password/${resetPasswordToken}`, + user.lastLocale ?? i18n.defaultLocale + ), + html: html( + user.username ?? email, + `${env.VERCEL_URL ?? env.BASE_URL}/reset-password/${resetPasswordToken}`, + user.lastLocale ?? i18n.defaultLocale + ), }) return { email } diff --git a/packages/app/src/app/[lang]/(protected)/layout.tsx b/packages/app/src/app/[lang]/(protected)/layout.tsx index 0ef21d10..ec56f87e 100644 --- a/packages/app/src/app/[lang]/(protected)/layout.tsx +++ b/packages/app/src/app/[lang]/(protected)/layout.tsx @@ -3,6 +3,8 @@ import { Locale } from "i18n-config" import requireAuth from "@/components/auth/require-auth" import LocaleSwitcher from "@/components/locale-switcher" import { ThemeSwitch } from "@/components/theme/theme-switch" +import { prisma } from "@/lib/prisma" +import { redis } from "@/lib/redis" export default async function ProtectedLayout({ children, @@ -13,7 +15,38 @@ export default async function ProtectedLayout({ lang: Locale } }) { - await requireAuth() + const session = await requireAuth() + + //* Set last locale + // Get last locale from redis or db + const getLastLocale = async () => { + const lastLocale = await redis.get(`lastLocale:${session.user.id}`) + if (lastLocale) { + return lastLocale + } + const lastLocaleFromDb = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { lastLocale: true }, + }) + if (lastLocaleFromDb && lastLocaleFromDb.lastLocale) { + await redis.set(`lastLocale:${session.user.id}`, lastLocaleFromDb.lastLocale) + return lastLocaleFromDb.lastLocale + } + return null + } + // Set last locale in redis and db + const setLastLocale = async (locale: Locale) => { + await redis.set(`lastLocale:${session.user.id}`, locale) + await prisma.user.update({ + where: { id: session.user.id }, + data: { lastLocale: locale }, + }) + } + + const lastLocale = await getLastLocale() + if (lastLocale !== lang) { + await setLastLocale(lang) + } return ( <> diff --git a/packages/app/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx b/packages/app/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx index ee807fae..0828f35f 100644 --- a/packages/app/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx +++ b/packages/app/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx @@ -37,7 +37,7 @@ export default async function SignupByCredentials({ {dictionary.signUpPage.createAnAccount} - + diff --git a/packages/app/src/app/[lang]/(sys-auth)/sign-up/page.tsx b/packages/app/src/app/[lang]/(sys-auth)/sign-up/page.tsx index e25b4abf..e0eca5e3 100644 --- a/packages/app/src/app/[lang]/(sys-auth)/sign-up/page.tsx +++ b/packages/app/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -39,7 +39,7 @@ export default async function SignUpPage({

{dictionary.signUpPage.enterEmail}

- +
diff --git a/packages/app/src/components/auth/register-user-auth-form.tsx b/packages/app/src/components/auth/register-user-auth-form.tsx index 8d647905..fbe40fff 100644 --- a/packages/app/src/components/auth/register-user-auth-form.tsx +++ b/packages/app/src/components/auth/register-user-auth-form.tsx @@ -23,6 +23,7 @@ type UserAuthFormProps = React.HTMLAttributes & { dictionary: TDictionary isMinimized?: boolean searchParams?: { [key: string]: string | string[] | undefined } + locale: string } export const formSchema = (dictionary: TDictionary) => @@ -52,7 +53,7 @@ export const getFormSchema = ({ dictionary, isMinimized }: { dictionary: TDictio export type IForm = z.infer> export type IFormMinimized = z.infer> -export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, ...props }: UserAuthFormProps) { +export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, locale, ...props }: UserAuthFormProps) { const router = useRouter() const registerMutation = trpc.auth.register.useMutation({ @@ -102,6 +103,7 @@ export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, .. username: "", password: "", confirmPassword: "", + locale, }, }) diff --git a/packages/app/src/lib/schemas/auth.ts b/packages/app/src/lib/schemas/auth.ts index dfed1de3..69209173 100644 --- a/packages/app/src/lib/schemas/auth.ts +++ b/packages/app/src/lib/schemas/auth.ts @@ -43,6 +43,7 @@ export const signUpSchema = (dictionary?: TDictionary) => signInSchema(dictionary).extend({ username: usernameSchema(dictionary), password: passwordSchemaWithRegex(dictionary), + locale: z.string(), }) export const signUpResponseSchema = (dictionary?: TDictionary) => diff --git a/packages/app/src/lib/templates/mail/reset-password.ts b/packages/app/src/lib/templates/mail/reset-password.ts index 3bf12fe8..7d22623b 100644 --- a/packages/app/src/lib/templates/mail/reset-password.ts +++ b/packages/app/src/lib/templates/mail/reset-password.ts @@ -2,7 +2,8 @@ import { env } from "env.mjs" export const subject = "Reset your password" -export const plainText = (username: string, resetLink: string) => `Password Reset +export const plainText = (username: string, resetLink: string, locale: string) => { + const en = `Password Reset Hello ${username}, @@ -13,12 +14,29 @@ ${resetLink} If you did not request this password reset, you can safely ignore this email. This email was sent to you as part of our account services.${ - env.SUPPORT_EMAIL ? ` If you have any questions, please contact us at ${env.SUPPORT_EMAIL}.` : "" -} + env.SUPPORT_EMAIL ? ` If you have any questions, please contact us at ${env.SUPPORT_EMAIL}.` : "" + } ` + const fr = `Réinitialiser votre mot de passe + +Bonjour ${username}, + +Nous avons reçu une demande de réinitialisation de votre mot de passe. Vous pouvez réinitialiser votre mot de passe en cliquant sur le lien suivant : + +${resetLink} + +Si vous n'avez pas demandé cette réinitialisation de mot de passe, vous pouvez ignorer cet e-mail en toute sécurité. + +Ce courriel vous a été envoyé dans le cadre de nos services de compte.${ + env.SUPPORT_EMAIL ? ` Si vous avez des questions, veuillez nous contacter à l'adresse ${env.SUPPORT_EMAIL}.` : "" + } +` + if (locale === "fr") return fr + return en +} -export const html = (username: string, resetLink: string) => ` - +export const html = (username: string, resetLink: string, locale: string) => ` + @@ -134,7 +152,13 @@ export const html = (username: string, resetLink: string) => `

- Forgot Your Password? + + ${ + locale === "fr" + ? "Mot de passe oublié ?" + : "Forgot Your Password?" + } +

@@ -175,7 +199,12 @@ export const html = (username: string, resetLink: string) => ` style="color:#848484;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;font-size:14px;line-height:180%;text-align:center;mso-line-height-alt:25.2px;">

- Hi ${username}, you recently requested to reset your password for your account. Click the button below to reset it. + ${ + locale === "fr" + ? `Bonjour ${username}, vous avez récemment demandé à réinitialiser votre mot de passe pour votre compte. Cliquez sur le bouton ci-dessous pour le réinitialiser.` + : ` + Hi ${username}, you recently requested to reset your password for your account. Click the button below to reset it.` + }

@@ -195,8 +224,13 @@ export const html = (username: string, resetLink: string) => ` href="${resetLink}" target="_blank" style="text-decoration:none;display:inline-block;color:#ffffff;background-color:#101;border-radius:4px;width:auto;border-top:1px solid #101;font-weight:undefined;border-right:1px solid #101;border-bottom:1px solid #101;border-left:1px solid #101;padding-top:5px;padding-bottom:5px;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;font-size:16px;text-align:center;mso-border-alt:none;word-break:keep-all;">Reset - Password + style="word-break: break-word; line-height: 32px;"> + ${ + locale === "fr" + ? "Réinitialiser le mot de passe" + : "Reset Password" + } +
diff --git a/packages/app/src/lib/templates/mail/verify-email.ts b/packages/app/src/lib/templates/mail/verify-email.ts index 8075366f..f9c8543b 100644 --- a/packages/app/src/lib/templates/mail/verify-email.ts +++ b/packages/app/src/lib/templates/mail/verify-email.ts @@ -2,7 +2,8 @@ import { env } from "env.mjs" export const subject = "Verify your email address" -export const plainText = (verificationLink: string) => `Email Verification +export const plainText = (verificationLink: string, locale: string) => { + const en = `Email Verification Hello, @@ -13,12 +14,31 @@ ${verificationLink} If you did not sign up for an account, you can safely ignore this email. This email was sent to you as part of our account services. ${ - env.SUPPORT_EMAIL ? ` If you have any questions, please contact us at ${env.SUPPORT_EMAIL}.` : "" -} + env.SUPPORT_EMAIL ? ` If you have any questions, please contact us at ${env.SUPPORT_EMAIL}.` : "" + } ` -export const html = (verificationLink: string) => ` - + const fr = `Vérification de l'adresse e-mail + +Bonjour, + +Merci de vous être inscrit chez nous. Veuillez vérifier votre adresse e-mail en cliquant sur le lien suivant : + +${verificationLink} + +Si vous ne vous êtes pas inscrit pour un compte, vous pouvez ignorer cet e-mail en toute sécurité. + +Cet e-mail vous a été envoyé dans le cadre de nos services de compte. ${ + env.SUPPORT_EMAIL ? ` Si vous avez des questions, veuillez nous contacter à ${env.SUPPORT_EMAIL}.` : "" + } +` + + if (locale === "fr") return fr + return en +} + +export const html = (verificationLink: string, locale: string) => ` + @@ -133,7 +153,11 @@ export const html = (verificationLink: string) => `

- Verify your email + ${ + locale === "fr" + ? "Vérification de l'adresse e-mail" + : "Email Verification" + }

@@ -174,8 +198,11 @@ export const html = (verificationLink: string) => ` style="color:#848484;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;font-size:14px;line-height:180%;text-align:center;mso-line-height-alt:25.2px;">

- Click the button below to verify your email - and complete your registration. + ${ + locale === "fr" + ? `Cliquez sur le bouton ci-dessous pour vérifier votre adresse e-mail et terminer votre inscription.` + : `Click the button below to verify your email and complete your registration.` + }

@@ -195,9 +222,11 @@ export const html = (verificationLink: string) => ` href="${verificationLink}" target="_blank" style="text-decoration:none;display:inline-block;color:#ffffff;background-color:#101;border-radius:4px;width:auto;border-top:1px solid #101;font-weight:undefined;border-right:1px solid #101;border-bottom:1px solid #101;border-left:1px solid #101;padding-top:5px;padding-bottom:5px;font-family:Arial, Helvetica Neue, Helvetica, sans-serif;font-size:16px;text-align:center;mso-border-alt:none;word-break:keep-all;">Confirm - my - email + style="word-break: break-word; line-height: 32px;">${ + locale === "fr" + ? "Vérifier mon adresse e-mail" + : "Confirm my email" + }