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
35 changes: 35 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@
"password": "Password",
"passwordConfirm": "Confirm password",
"registerButton": "Register",
"email": "Email (optional)",
"username": "Username",
"validation": {
"password": {
Expand All @@ -264,10 +265,44 @@
"min": "Username must be at least {{min, number}} characters long",
"regex": "Username must only contain alphanumeric characters",
"required": "Username is required"
},
"email": {
"invalid": "Invalid email"
}
},
"usernameTaken": "Username is taken"
},
"passwordResetRequestPage": {
"rateLimited": "You cannot request a password reset again yet",
"email": "Email",
"title": "Request password reset",
"success": "Email sent",
"emailSent": "Password reset email sent",
"sendEmailButton": "Send password reset email",
"validation": {
"email": {
"invalid": "Invalid email"
}
}
},
"passwordResetPage": {
"rateLimited": "You cannot request a password reset again yet",
"invalidToken": "Your password reset token is invalid or expired",
"changePassword": "Change password",
"title": "Reset password",
"success": "Password changed",
"validation": {
"password": {
"max": "Password can not be more than {{max, number}} characters long",
"min": "Password must be at least {{min, number}} characters long",
"required": "Password is required"
},
"passwordConfirm": {
"noMatch": "Passwords must match",
"required": "Password confirmation is required"
}
}
},
"theme": {
"dark": "Dark mode",
"light": "Light mode",
Expand Down
35 changes: 35 additions & 0 deletions public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
"passwordConfirm": "Vahvista salasana",
"registerButton": "Rekisteröidy",
"username": "Käyttäjänimi",
"email": "Sähköposti (valinnainen)",
"validation": {
"password": {
"max": "Salasana voi olla enintään {{max, number}} merkkiä pitkä",
Expand All @@ -264,10 +265,44 @@
"min": "Käyttäjänimen tulee olla vähintään {{min, number}} merkkiä pitkä",
"regex": "Käyttäjänimi voi sisältää vain alphanumeerisia merkkejä",
"required": "Käyttäjänimi vaaditaan"
},
"email": {
"invalid": "Virheellinen sähköpostiosoite"
}
},
"usernameTaken": "Käyttäjänimi on varattu"
},
"passwordResetRequestPage": {
"rateLimited": "Et voi pyytää uutta salasanan nollausta vielä",
"email": "Email",
"success": "Sähköposti lähetetty",
"title": "Salasanan nollaus",
"emailSent": "Salasanan nollaussähköposti lähetetty",
"sendEmailButton": "Lähetä salasanan nollaussähköposti",
"validation": {
"email": {
"invalid": "Virheellinen sähköpostiosoite"
}
}
},
"passwordResetPage": {
"rateLimited": "Et voi pyytää uutta salasanan nollausta vielä",
"invalidToken": "Koodisi on virheellinen tai ei enää voimassa",
"changePassword": "Vaihda salasana",
"title": "Salasanan nollaus",
"success": "Salasana muutettu",
"validation": {
"password": {
"max": "Salasana voi olla enintään {{max, number}} merkkiä pitkä",
"min": "Salasanan tulee olla vähintään {{min, number}} merkkiä pitkä",
"required": "Salasana vaaditaan"
},
"passwordConfirm": {
"noMatch": "Salasanat eivät täsmää",
"required": "Salasanavahvistus vaaditaan"
}
}
},
"theme": {
"dark": "Tumma teema",
"light": "Vaalea teema",
Expand Down
22 changes: 21 additions & 1 deletion src/app/[locale]/register/RegistrationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const RegistrationForm = () => {
<Formik
initialValues={{
username: "",
email: "",
password: "",
passwordConfirmation: "",
}}
Expand All @@ -31,6 +32,9 @@ export const RegistrationForm = () => {
/^[a-zA-Z0-9]*$/,
t("registrationPage.validation.username.regex"),
),
email: Yup.string()
.email(t("registrationPage.validation.email.invalid"))
.optional(),
password: Yup.string()
.required(t("registrationPage.validation.password.required"))
.min(8, t("registrationPage.validation.password.min", { min: 8 }))
Expand All @@ -47,7 +51,18 @@ export const RegistrationForm = () => {
})}
onSubmit={async (values) => {
setVisible(true);
const result = await register(values.username, values.password);

let result;
if (values.email == "") {
result = await register(values.username, values.password);
} else {
result = await register(
values.username,
values.password,
values.email,
);
}

switch (result) {
case RegistrationResult.RateLimited:
showNotification({
Expand Down Expand Up @@ -82,6 +97,11 @@ export const RegistrationForm = () => {
name="username"
label={t("registrationPage.username")}
/>
<FormikTextInput
name="email"
label={t("registrationPage.email")}
mt={15}
/>
<FormikPasswordInput
name="password"
label={t("registrationPage.password")}
Expand Down
22 changes: 17 additions & 5 deletions src/app/[locale]/register/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ interface ApiAuthLoginResponse {
is_public: boolean;
}

export const register = async (username: string, password: string) => {
export const register = async (
username: string,
password: string,
email?: string,
) => {
const body = email
? JSON.stringify({
username: username,
email: email,
password: password,
})
: JSON.stringify({
username: username,
password: password,
});

const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/auth/register",
{
Expand All @@ -25,10 +40,7 @@ export const register = async (username: string, password: string) => {
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
body: JSON.stringify({
username: username,
password: password,
}),
body: body,
},
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { Form, Formik } from "formik";
import { useTranslation } from "react-i18next";
import * as Yup from "yup";
import { requestPasswordReset } from "./actions";
import { Button, LoadingOverlay } from "@mantine/core";
import { useState } from "react";
import { showNotification } from "@mantine/notifications";
import { PasswordResetRequestResult } from "../../../types";
import { FormikTextInput } from "../../../components/forms/FormikTextInput";

export const PasswordResetRequestForm = () => {
const [visible, setVisible] = useState(false);
const { t } = useTranslation();

return (
<Formik
initialValues={{
email: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.email(t("passwordResetRequestPage.validation.email.invalid"))
.required(),
})}
onSubmit={async (values) => {
const result = await requestPasswordReset(values.email);
switch (result) {
case PasswordResetRequestResult.RateLimited:
showNotification({
title: t("error"),
color: "red",
message: t("passwordResetRequestPage.rateLimited"),
});
setVisible(false);
break;
case PasswordResetRequestResult.UnknownError:
showNotification({
title: t("error"),
color: "red",
message: t("unknownErrorOccurred"),
});
setVisible(false);
break;
case PasswordResetRequestResult.Success:
showNotification({
title: t("passwordResetRequestPage.success"),
color: "green",
message: t("passwordResetRequestPage.emailSent"),
});
setVisible(false);
break;
}
}}
>
{() => (
<Form style={{ width: "100%" }}>
<FormikTextInput
name="email"
label={t("passwordResetRequestPage.email")}
style={{ width: "100%" }}
/>
<Button mt={20} type="submit">
<LoadingOverlay visible={visible} />
{t("passwordResetRequestPage.sendEmailButton")}
</Button>
</Form>
)}
</Formik>
);
};
32 changes: 32 additions & 0 deletions src/app/[locale]/request-password-reset/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use server";

import { headers } from "next/headers";
import { PasswordResetRequestResult } from "../../../types";

export const requestPasswordReset = async (email: string) => {
const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/auth/reset-password",
{
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
"client-ip": headers().get("client-ip") ?? "Unknown IP",
"bypass-token": process.env.RATELIMIT_IP_FORWARD_SECRET ?? "",
},
body: JSON.stringify({
email: email,
}),
},
);

if (!response.ok) {
if (response.status == 429) {
return PasswordResetRequestResult.RateLimited;
}

return PasswordResetRequestResult.UnknownError;
}

return PasswordResetRequestResult.Success;
};
20 changes: 20 additions & 0 deletions src/app/[locale]/request-password-reset/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Title, Group } from "@mantine/core";
import { PasswordResetRequestForm } from "./PasswordResetRequestForm";
import initTranslations from "../../i18n";

export default async function PasswordResetRequestPage({
params: { locale },
}: {
params: { locale: string };
}) {
const { t } = await initTranslations(locale, ["common"]);

return (
<Group>
<Title order={1} mb={20}>
{t("passwordResetRequestPage.title")}
</Title>
<PasswordResetRequestForm />
</Group>
);
}
Loading