Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
1,315 changes: 941 additions & 374 deletions bun.lock

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -59,6 +60,8 @@
"signIn": "Sign in",
"useYourAccount": "Use your account",
"forgotPassword": "Forgot password",
"forgotPasswordDescription": "Enter your Hackerspace account email address to reset your password.",
"enterTheCode": "Please enter the code we've sent to your email to confirm the password change",
"submit": "Submit",
"invalidCredentials": "Invalid credentials",
"matrixRegistrationFailed": "Matrix user registration failed",
Expand All @@ -75,6 +78,8 @@
"noVerificationRequest": "No verification request found",
"verificationCodeExpired": "The verification code was expired. We sent another code to your inbox",
"emailUpdateSuccess": "Email updated successfully",
"passwordUpdateSuccess": "Password updated successfully! You may now log in using your new password.",
"tryAgain": "Try again",
"form": {
"username": {
"label": "Username",
Expand All @@ -94,12 +99,21 @@
"mismatch": "Passwords do not match",
"weak": "Password is too weak"
},
"newPassword": {
"label": "New password"
},
"otp": {
"label": "One-time password",
"required": "One-time password is required",
"length": "One-time password must be {count} characters long",
"invalid": "One-time password must only contain digits and characters",
"incorrect": "Incorrect one-time password"
"incorrect": "Incorrect one-time password",
"expired": "One-time password has expired."
},
"email": {
"label": "Email",
"required": "Email is required",
"invalid": "Email is invalid"
}
},
"error": {
Expand All @@ -109,7 +123,12 @@
"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",
"forgotPasswordRequestIdRequired": "Forgot password request ID is required",
"matrixPasswordUpdateFailed": "Failed to update Matrix password",
"passwordUpdateFailed": "Failed to update password"
}
},
"layout": {
Expand All @@ -135,6 +154,7 @@
"profile": "Profile",
"signIn": "Sign in",
"signOut": "Sign out",
"forgotPassword": "Forgot password",
"settings": "Settings",
"management": "Management",
"links": "Links",
Expand Down
24 changes: 22 additions & 2 deletions messages/nb-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -59,6 +60,8 @@
"signIn": "Logg inn",
"useYourAccount": "Bruk din konto",
"forgotPassword": "Glemt passord",
"forgotPasswordDescription": "Skriv inn e-postadressen knyttet til Hackerspace-kontoen din for å tilbakestille passordet.",
"enterTheCode": "Vennligst skriv inn koden vi har sendt til e-posten din for å bekrefte endringen av passordet",
"submit": "Send",
"invalidCredentials": "Ugyldige påloggingsdetaljer",
"matrixRegistrationFailed": "Matrix bruker registrering feilet",
Expand All @@ -75,6 +78,8 @@
"noVerificationRequest": "Ingen verifikasjonsforespørsel funnet",
"verificationCodeExpired": "Verifikasjonskoden har utløpt, vi har sendt en ny kode til innboksen din",
"emailUpdateSuccess": "E-postadressen ble oppdatert",
"passwordUpdateSuccess": "Passordet ble oppdatert! Du kan nå logge inn med det nye passordet.",
"tryAgain": "Prøv igjen",
"form": {
"username": {
"label": "Brukernavn",
Expand All @@ -94,12 +99,21 @@
"mismatch": "Passordene stemmer ikke overens",
"weak": "Passordet er for svakt"
},
"newPassword": {
"label": "Nytt passord"
},
"otp": {
"label": "Engangspassord",
"required": "Engangspassord er påkrevd",
"length": "Engangspassordet må være {count} tegn langt",
"invalid": "Engangspassordet må kun inneholde sifre og bokstaver",
"incorrect": "Feil engangspassord"
"incorrect": "Feil engangspassord",
"expired": "Engangspassordet har utløpt"
},
"email": {
"label": "E-post",
"required": "E-post er påkrevd",
"invalid": "E-post er ugyldig"
}
},
"error": {
Expand All @@ -109,7 +123,12 @@
"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",
"forgotPasswordRequestIdRequired": "ID-en til forespørselen om tilbakestilling av passord er påkrevd",
"matrixPasswordUpdateFailed": "Kunne ikke oppdatere Matrix-passord",
"passwordUpdateFailed": "Kunne ikke oppdatere passord"
}
},
"layout": {
Expand All @@ -135,6 +154,7 @@
"profile": "Profil",
"signIn": "Logg inn",
"signOut": "Logg ut",
"forgotPassword": "Glemt passord",
"settings": "Innstillinger",
"management": "Administrasjon",
"links": "Lenker",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-tooltip": "^1.2.6",
"@react-email/components": "0.0.34",
"@react-email/components": "0.5.1",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-form": "^1.11.0",
"@tanstack/react-query": "^5.75.7",
Expand Down Expand Up @@ -109,6 +109,7 @@
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@faker-js/faker": "^9.7.0",
"@react-email/preview-server": "4.2.8",
"@tailwindcss/postcss": "^4.1.6",
"@types/bun": "^1.2.12",
"@types/nodemailer": "^6.4.17",
Expand All @@ -120,7 +121,7 @@
"drizzle-seed": "^0.3.1",
"lefthook": "^1.11.12",
"postcss": "^8.5.3",
"react-email": "3.0.6",
"react-email": "4.2.8",
"react-scan": "^0.2.14",
"server-only": "^0.0.1",
"tailwindcss": "^4.1.6",
Expand Down
30 changes: 30 additions & 0 deletions src/app/[locale]/auth/forgot-password/[requestId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Locale } from 'next-intl';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { NewPasswordForm } from '@/components/auth/NewPasswordForm';
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 ForgotPasswordRequestPage({
params,
}: {
params: Promise<{ locale: Locale; requestId: string }>;
}) {
const { locale, requestId } = await params;
setRequestLocale(locale);

const { user } = await api.auth.state();

if (user) {
redirect({ href: '/', locale });
}

return <NewPasswordForm requestId={requestId} />;
}
15 changes: 10 additions & 5 deletions src/app/[locale]/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { Locale } from 'next-intl';
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('auth');
Expand All @@ -17,9 +20,11 @@ export default async function ForgotPasswordPage({
const { locale } = await params;
setRequestLocale(locale as Locale);

return (
<div className='flex h-full flex-col transition-opacity duration-500'>
forgot password page
</div>
);
const { user } = await api.auth.state();

if (user) {
redirect({ href: '/', locale: locale as Locale });
}

return <ForgotPasswordForm />;
}
8 changes: 0 additions & 8 deletions src/app/[locale]/auth/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ type AuthLayoutProps = {
params: Promise<{ locale: string }>;
};

export async function generateMetadata() {
const t = await getTranslations('layout');

return {
title: t('signIn'),
};
}

export const dynamic = 'force-dynamic';

export default async function AuthLayout({
Expand Down
95 changes: 95 additions & 0 deletions src/components/auth/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'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),
onSuccess: (id) =>
router.push({
pathname: '/auth/forgot-password/[requestId]',
params: { requestId: id },
}),
});

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 (
<div
className={`flex h-full flex-col transition-opacity duration-500 ${isPending ? 'pointer-events-none opacity-50' : ''}`}
>
<div className='mb-4 space-y-2 text-center'>
<h1 className='text-4xl'>{t('forgotPassword')}</h1>
<p className='text-sm'>{t('forgotPasswordDescription')}</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='relative grow space-y-6'
>
<form.AppForm>
<form.AppField name='email'>
{(field) => (
<field.TextField
label={t('form.email.label')}
placeholder='[email protected]'
autoComplete='email'
/>
)}
</form.AppField>
<div className='absolute bottom-0 flex w-full xs:flex-row-reverse flex-col justify-between gap-2'>
<form.SubmitButton>{t('submit')}</form.SubmitButton>
<Link
variant='secondary'
size='default'
href='/auth/account'
className='gap-2'
>
{tUi('goBack')}
</Link>
</div>
</form.AppForm>
</form>
</div>
);
}

export { ForgotPasswordForm };
Loading