From 206e013639e0160737d610278ce3fff2db96040b Mon Sep 17 00:00:00 2001 From: ZeroWave022 <36341766+ZeroWave022@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:34:19 +0200 Subject: [PATCH 01/14] feat: Add ForgotPasswordForm and create request procedure --- messages/en-GB.json | 12 ++- messages/nb-NO.json | 12 ++- src/app/[locale]/auth/account/page.tsx | 10 ++- .../[locale]/auth/forgot-password/page.tsx | 26 ++++-- src/app/[locale]/auth/layout.tsx | 8 -- src/app/[locale]/auth/page.tsx | 8 ++ src/components/auth/ForgotPasswordForm.tsx | 90 +++++++++++++++++++ src/emails/ForgotPasswordEmail.tsx | 67 ++++++++++++++ src/server/api/index.ts | 2 + src/server/api/routers/forgotPassword.ts | 77 ++++++++++++++++ src/server/api/routers/index.ts | 1 + src/server/db/tables/forgotPassword.ts | 47 ++++++++++ src/server/db/tables/index.ts | 1 + src/server/db/tables/users.ts | 2 + src/validations/auth/forgotPasswordSchema.ts | 14 +++ 15 files changed, 360 insertions(+), 17 deletions(-) create mode 100644 src/components/auth/ForgotPasswordForm.tsx create mode 100644 src/emails/ForgotPasswordEmail.tsx create mode 100644 src/server/api/routers/forgotPassword.ts create mode 100644 src/server/db/tables/forgotPassword.ts create mode 100644 src/validations/auth/forgotPasswordSchema.ts diff --git a/messages/en-GB.json b/messages/en-GB.json index 0a1cdf83..9a745f04 100644 --- a/messages/en-GB.json +++ b/messages/en-GB.json @@ -6,6 +6,7 @@ "open": "Open", "close": "Close", "previous": "Previous", + "goBack": "Go back", "goToPreviousPage": "Go to previous page", "next": "Next", "goToNextPage": "Go to next page", @@ -57,6 +58,7 @@ "signIn": "Sign in", "useYourAccount": "Use your account", "forgotPassword": "Forgot password", + "forgotPasswordDescription": "Enter your Hackerspace account email address to reset your password.", "submit": "Submit", "invalidCredentials": "Invalid credentials", "matrixRegistrationFailed": "Matrix user registration failed", @@ -98,6 +100,11 @@ "length": "One-time password must be {count} characters long", "invalid": "One-time password must only contain digits and characters", "incorrect": "Incorrect one-time password" + }, + "email": { + "label": "Email", + "required": "Email is required", + "invalid": "Email is invalid" } }, "error": { @@ -107,7 +114,9 @@ "phoneInUse": "Phone number is already in use", "emailInUse": "Email is already in use", "invalidUserData": "Invalid user data", - "userCreationFailed": "Failed to create user" + "userCreationFailed": "Failed to create user", + "forgotPasswordRequestCreationFailed": "Failed to create forgot password request", + "failedToSendEmail": "Failed to send email" } }, "layout": { @@ -130,6 +139,7 @@ "profile": "Profile", "signIn": "Sign in", "signOut": "Sign out", + "forgotPassword": "Forgot password", "settings": "Settings", "links": "Links", "utilities": "Utilities", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 48813f25..16a90919 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -6,6 +6,7 @@ "open": "Åpne", "close": "Lukk", "previous": "Forrige", + "goBack": "Gå tilbake", "goToPreviousPage": "Gå til forrige side", "next": "Neste", "goToNextPage": "Gå til neste side", @@ -57,6 +58,7 @@ "signIn": "Logg inn", "useYourAccount": "Bruk din konto", "forgotPassword": "Glemt passord", + "forgotPasswordDescription": "Skriv inn e-postadressen knyttet til Hackerspace-kontoen din for å tilbakestille passordet.", "submit": "Send", "invalidCredentials": "Ugyldige påloggingsdetaljer", "matrixRegistrationFailed": "Matrix bruker registrering feilet", @@ -98,6 +100,11 @@ "length": "Engangspassordet må være {count} tegn langt", "invalid": "Engangspassordet må kun inneholde sifre og bokstaver", "incorrect": "Feil engangspassord" + }, + "email": { + "label": "E-post", + "required": "E-post er påkrevd", + "invalid": "E-post er ugyldig" } }, "error": { @@ -107,7 +114,9 @@ "phoneInUse": "Telefonnummeret er allerede i bruk", "emailInUse": "E-postadressen er allerede i bruk", "invalidUserData": "Ugyldig brukerdata", - "userCreationFailed": "Kunne ikke opprette bruker" + "userCreationFailed": "Kunne ikke opprette bruker", + "forgotPasswordRequestCreationFailed": "Kunne ikke opprette forespørsel om tilbakestilling av passord", + "failedToSendEmail": "Kunne ikke sende e-post" } }, "layout": { @@ -130,6 +139,7 @@ "profile": "Profil", "signIn": "Logg inn", "signOut": "Logg ut", + "forgotPassword": "Glemt passord", "settings": "Innstillinger", "links": "Lenker", "utilities": "Verktøy", diff --git a/src/app/[locale]/auth/account/page.tsx b/src/app/[locale]/auth/account/page.tsx index 7010a93c..420d06e9 100644 --- a/src/app/[locale]/auth/account/page.tsx +++ b/src/app/[locale]/auth/account/page.tsx @@ -1,9 +1,17 @@ import type { Locale } from 'next-intl'; -import { setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; import { AccountSignInForm } from '@/components/auth/AccountSignInForm'; import { api } from '@/lib/api/server'; import { redirect } from '@/lib/locale/navigation'; +export async function generateMetadata() { + const t = await getTranslations('layout'); + + return { + title: t('signIn'), + }; +} + export default async function AccountPage({ params, }: { diff --git a/src/app/[locale]/auth/forgot-password/page.tsx b/src/app/[locale]/auth/forgot-password/page.tsx index 71601647..65667f05 100644 --- a/src/app/[locale]/auth/forgot-password/page.tsx +++ b/src/app/[locale]/auth/forgot-password/page.tsx @@ -1,5 +1,16 @@ import type { Locale } from 'next-intl'; -import { setRequestLocale } from 'next-intl/server'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; +import { ForgotPasswordForm } from '@/components/auth/ForgotPasswordForm'; +import { api } from '@/lib/api/server'; +import { redirect } from '@/lib/locale/navigation'; + +export async function generateMetadata() { + const t = await getTranslations('layout'); + + return { + title: t('forgotPassword'), + }; +} export default async function ForgotPasswordPage({ params, @@ -8,9 +19,12 @@ export default async function ForgotPasswordPage({ }) { const { locale } = await params; setRequestLocale(locale); - return ( -
- forgot password page -
- ); + + const { user } = await api.auth.state(); + + if (user) { + redirect({ href: '/', locale }); + } + + return ; } diff --git a/src/app/[locale]/auth/layout.tsx b/src/app/[locale]/auth/layout.tsx index 9557b2d1..400894b5 100644 --- a/src/app/[locale]/auth/layout.tsx +++ b/src/app/[locale]/auth/layout.tsx @@ -15,14 +15,6 @@ type AuthLayoutProps = { params: Promise<{ locale: Locale }>; }; -export async function generateMetadata() { - const t = await getTranslations('layout'); - - return { - title: t('signIn'), - }; -} - export const dynamic = 'force-dynamic'; export default async function AuthLayout({ diff --git a/src/app/[locale]/auth/page.tsx b/src/app/[locale]/auth/page.tsx index 22745b22..87a745e2 100644 --- a/src/app/[locale]/auth/page.tsx +++ b/src/app/[locale]/auth/page.tsx @@ -8,6 +8,14 @@ import { Separator } from '@/components/ui/Separator'; import { api } from '@/lib/api/server'; import { redirect } from '@/lib/locale/navigation'; +export async function generateMetadata() { + const t = await getTranslations('layout'); + + return { + title: t('signIn'), + }; +} + export default async function SignInPage({ params, searchParams, diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 00000000..dd178d27 --- /dev/null +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { useTheme } from 'next-themes'; +import { usePending } from '@/components/auth/PendingBar'; +import { useAppForm } from '@/components/ui/Form'; +import { Link } from '@/components/ui/Link'; +import { api } from '@/lib/api/client'; +import type { TRPCClientError } from '@/lib/api/types'; +import { useRouter } from '@/lib/locale/navigation'; +import { forgotPasswordSchema } from '@/validations/auth/forgotPasswordSchema'; + +function ForgotPasswordForm() { + const router = useRouter(); + const t = useTranslations('auth'); + const tUi = useTranslations('ui'); + const formSchema = forgotPasswordSchema(useTranslations()); + const { resolvedTheme } = useTheme(); + const { isPending, setPending } = usePending(); + + const createRequestMutation = api.forgotPassword.createRequest.useMutation({ + onMutate: () => setPending(true), + onSettled: () => setPending(false), + }); + + const form = useAppForm({ + validators: { + onChange: formSchema, + onSubmitAsync: async ({ value }) => { + try { + await createRequestMutation.mutateAsync(value); + } catch (error: unknown) { + setPending(false); + const TRPCError = error as TRPCClientError; + if (!TRPCError.data?.toast) { + return { fields: { email: { message: TRPCError.message } } }; + } + return ' '; + } + }, + }, + defaultValues: { + email: '', + theme: resolvedTheme as 'light' | 'dark', + }, + }); + + return ( +
+
+

{t('forgotPassword')}

+

{t('forgotPasswordDescription')}

+
+
{ + e.preventDefault(); + form.handleSubmit(); + }} + className='relative grow space-y-6' + > + + + {(field) => ( + + )} + +
+ {t('submit')} + + {tUi('goBack')} + +
+
+
+
+ ); +} + +export { ForgotPasswordForm }; diff --git a/src/emails/ForgotPasswordEmail.tsx b/src/emails/ForgotPasswordEmail.tsx new file mode 100644 index 00000000..10212a1e --- /dev/null +++ b/src/emails/ForgotPasswordEmail.tsx @@ -0,0 +1,67 @@ +import { Section, Text } from '@react-email/components'; +import type { Locale } from 'next-intl'; +import { Footer } from '@/components/emails/Footer'; +import { Header } from '@/components/emails/Header'; +import { Wrapper } from '@/components/emails/Wrapper'; +import { routing } from '@/lib/locale'; + +function ForgotPasswordEmail({ + locale = routing.defaultLocale, + theme = 'dark', + publicSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', + validationCode, +}: { + locale: Locale; + theme: 'dark' | 'light'; + publicSiteUrl?: string; + validationCode: string; +}) { + return ( + { + switch (locale) { + case 'nb-NO': + return 'Gjenopprett tilgang til kontoen din'; + default: + return 'Recover access to your account'; + } + })()} + > +
{ + switch (locale) { + case 'nb-NO': + return 'Gjenopprett tilgang til Hackerspace-kontoen din'; + default: + return 'Recover access to your Hackerspace Account'; + } + })()} + theme={theme} + publicSiteUrl={publicSiteUrl} + /> + + {(() => { + switch (locale) { + case 'nb-NO': + return 'Din bekreftelseskode er nedenfor - skriv den inn på nettsiden vår for å endre passordet ditt.'; + default: + return 'Your confirmation code is below - enter it on our website to reset your password.'; + } + })()} + +
+ + {validationCode.substring(0, 4)} + - + {validationCode.substring(4)} + +
+