Skip to content
Merged
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
1,315 changes: 941 additions & 374 deletions bun.lock

Large diffs are not rendered by default.

27 changes: 25 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,9 @@
"signIn": "Sign in",
"useYourAccount": "Use your account",
"forgotPassword": "Forgot password",
"forgotPasswordDescription": "Enter your Hackerspace account email address to reset your password.",
"forgotPasswordInfoToast": "We've sent you a one-time code to reset your password if the email <strong>{email}</strong> is associated with a Hackerspace account.<br></br>If you've used the wrong email, please create a new forgot password request.",
"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 +79,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 @@ -88,18 +94,29 @@
"required": "Password is required",
"minLength": "Password must be at least {count} characters",
"maxLength": "Password must be less than {count} characters",
"lowercase": "Password must contain at least one lowercase letter",
"uppercase": "Password must contain at least one uppercase letter",
"specialChar": "Password must contain at least one special character",
"confirmLabel": "Confirm password",
"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.",
"used": "One-time password has already been used. Please create a new forgot password request."
},
"email": {
"label": "Email",
"required": "Email is required",
"invalid": "Email is invalid"
}
},
"error": {
Expand All @@ -109,7 +126,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 +157,7 @@
"profile": "Profile",
"signIn": "Sign in",
"signOut": "Sign out",
"forgotPassword": "Forgot password",
"settings": "Settings",
"management": "Management",
"links": "Links",
Expand Down
27 changes: 25 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,9 @@
"signIn": "Logg inn",
"useYourAccount": "Bruk din konto",
"forgotPassword": "Glemt passord",
"forgotPasswordDescription": "Skriv inn e-postadressen knyttet til Hackerspace-kontoen din for å tilbakestille passordet.",
"forgotPasswordInfoToast": "Vi har sendt deg en engangskode for å tilbakestille passordet dersom e-postadressen <strong>{email}</strong> er knyttet til en Hackerspace-konto.<br></br>Hvis du brukte feil e-post, vennligst lag en ny forespørsel om tilbakestilling av passord.",
"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 +79,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 @@ -88,18 +94,29 @@
"required": "Passord er påkrevd",
"minLength": "Passord må være minst {count} tegn",
"maxLength": "Passord må være mindre enn {count} tegn",
"lowercase": "Passord må inneholde minst én liten bokstav",
"uppercase": "Passord må inneholde minst én stor bokstav",
"specialChar": "Passord må inneholde minst ett spesialtegn",
"confirmLabel": "Bekreft passord",
"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",
"used": "Engangspassordet har allerede blitt brukt. Vennligst lag en ny forespørsel om tilbakestilling av passord."
},
"email": {
"label": "E-post",
"required": "E-post er påkrevd",
"invalid": "E-post er ugyldig"
}
},
"error": {
Expand All @@ -109,7 +126,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 +157,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
105 changes: 105 additions & 0 deletions src/components/auth/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'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 { toast } from '@/components/ui/Toaster';
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) => {
toast.info(
t.rich('forgotPasswordInfoToast', {
br: () => <br />,
strong: (chunks) => <strong>{chunks}</strong>,
email: form.getFieldValue('email'),
}),
{ duration: 20_000 },
);
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) {
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