feat: email changing

This commit is contained in:
zkldi
2023-08-11 13:21:26 +01:00
parent 81d6a9f21d
commit 7fe682e8f0
6 changed files with 281 additions and 63 deletions
+13 -1
View File
@@ -52,6 +52,16 @@ export default function RegisterPage() {
return;
}
// user trying to gmail but can't use their keyboard
// like TEN people have made this mistake and then complained to me on discord
// what the hell?
//
// how do real websites deal with this?
if (values.email.match(/@gma/u) && !values.email.match(/@gmail\.com *$/u)) {
setErr("This email address is probably typo'd. Did you mean 'gmail'?");
return;
}
const rj = await APIFetchV1<UserDocument>(
"/auth/register",
{
@@ -71,7 +81,9 @@ export default function RegisterPage() {
true
);
recaptchaRef.current.reset();
if (recaptchaRef.current) {
recaptchaRef.current.reset();
}
if (!rj.success) {
setErr(HumaniseError(rj.description));
@@ -62,7 +62,7 @@ export default function UserSettingsDocumentPage({ reqUser }: Props) {
setValue={setPage}
id="account"
>
<Icon type="lock" /> Change Password
<Icon type="lock" /> Change Email/Password
</SelectButton>
</div>
</div>
@@ -91,7 +91,7 @@ export default function UserSettingsDocumentPage({ reqUser }: Props) {
}
function AccountSettings({ reqUser }: { reqUser: UserDocument }) {
const formik = useFormik({
const formikPassword = useFormik({
initialValues: {
"!oldPassword": "",
"!password": "",
@@ -112,7 +112,7 @@ function AccountSettings({ reqUser }: { reqUser: UserDocument }) {
);
if (r.success) {
formik.setValues({
formikPassword.setValues({
"!oldPassword": "",
"!password": "",
confPass: "",
@@ -121,60 +121,146 @@ function AccountSettings({ reqUser }: { reqUser: UserDocument }) {
},
});
const formikEmail = useFormik({
initialValues: {
"!password": "",
email: "",
confEmail: "",
},
onSubmit: async (values) => {
const r = await APIFetchV1<UserSettingsDocument>(
`/users/${reqUser.id}/change-email`,
{
method: "POST",
...FetchJSONBody({
"!password": values["!password"],
email: values.email,
}),
},
true,
true
);
if (r.success) {
formikEmail.setValues({
"!password": "",
email: "",
confEmail: "",
});
}
},
});
return (
<Form onSubmit={formik.handleSubmit} className="d-flex flex-column gap-4">
<Form.Group>
<Form.Label>Old Password</Form.Label>
<Form.Control
type="password"
id="!oldPassword"
value={formik.values["!oldPassword"]}
placeholder="Your Current Password"
onChange={formik.handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>New Password</Form.Label>
<Form.Control
type="password"
id="!password"
value={formik.values["!password"]}
placeholder="New Password"
onChange={formik.handleChange}
/>
{formik.values["!password"].length < 8 && (
<Form.Text className="text-warning">
Passwords have to be at least 8 characters long.
</Form.Text>
<>
<Form onSubmit={formikEmail.handleSubmit} className="d-flex flex-column gap-4">
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
id="!password"
value={formikEmail.values["!password"]}
placeholder="Your Current Password"
onChange={formikEmail.handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>New Email</Form.Label>
<Form.Control
type="email"
id="email"
value={formikEmail.values.email}
placeholder="New Email"
onChange={formikEmail.handleChange}
/>
{formikEmail.values["!password"].length < 8 && (
<Form.Text className="text-warning">
Passwords have to be at least 8 characters long.
</Form.Text>
)}
</Form.Group>
<Form.Group>
<Form.Label>Confirm New Email</Form.Label>
<Form.Control
type="email"
id="confEmail"
value={formikEmail.values.confEmail}
placeholder="New Email"
onChange={formikEmail.handleChange}
/>
</Form.Group>
{!(formikEmail.values.email === formikEmail.values.confEmail) && (
<Form.Text className="text-danger">Emails don't match!</Form.Text>
)}
</Form.Group>
<Form.Group>
<Form.Label>Confirm New Password</Form.Label>
<Form.Control
type="password"
id="confPass"
value={formik.values.confPass}
placeholder="New Password"
onChange={formik.handleChange}
/>
</Form.Group>
{!(formik.values["!password"] === formik.values.confPass) && (
<Form.Text className="text-danger">Passwords don't match!</Form.Text>
)}
<Button
className="mt-8"
variant="danger"
type="submit"
disabled={
!(
formik.values["!password"] === formik.values.confPass &&
formik.values.confPass.length >= 8
)
}
>
Change Password
</Button>
</Form>
<Button
className="mt-8"
variant="danger"
type="submit"
disabled={
!(
formikEmail.values.email === formikEmail.values.confEmail &&
formikEmail.values["!password"].length >= 8
)
}
>
Change Email
</Button>
</Form>
<Divider />
<Form onSubmit={formikPassword.handleSubmit} className="d-flex flex-column gap-4">
<Form.Group>
<Form.Label>Old Password</Form.Label>
<Form.Control
type="password"
id="!oldPassword"
value={formikPassword.values["!oldPassword"]}
placeholder="Your Current Password"
onChange={formikPassword.handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>New Password</Form.Label>
<Form.Control
type="password"
id="!password"
value={formikPassword.values["!password"]}
placeholder="New Password"
onChange={formikPassword.handleChange}
/>
{formikPassword.values["!password"].length < 8 && (
<Form.Text className="text-warning">
Passwords have to be at least 8 characters long.
</Form.Text>
)}
</Form.Group>
<Form.Group>
<Form.Label>Confirm New Password</Form.Label>
<Form.Control
type="password"
id="confPass"
value={formikPassword.values.confPass}
placeholder="New Password"
onChange={formikPassword.handleChange}
/>
</Form.Group>
{!(formikPassword.values["!password"] === formikPassword.values.confPass) && (
<Form.Text className="text-danger">Passwords don't match!</Form.Text>
)}
<Button
className="mt-8"
variant="danger"
type="submit"
disabled={
!(
formikPassword.values["!password"] === formikPassword.values.confPass &&
formikPassword.values.confPass.length >= 8
)
}
>
Change Password
</Button>
</Form>
</>
);
}
@@ -38,7 +38,7 @@ export default function EmailVerify({
<div className="col-12 col-lg-6 offset-lg-3 text-center" style={{ fontSize: "1.4rem" }}>
One last thing! You need to verify your email before you can use the site.
<br />
An email has been sent to the address you signed up with.
An email has been sent to the address you signed up with. (<code>{email}</code>)
<br />
It might've ended up in your spam, so check there too!
<br />
@@ -18,6 +18,10 @@ const BCRYPT_SALT_ROUNDS = 12;
export const ValidatePassword = (self: unknown) =>
(typeof self === "string" && self.length >= 8) || "Passwords must be 8 characters or more.";
const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
export const ValidateEmail = p.regex(LAZY_EMAIL_REGEX);
/**
* Compares a plaintext string of a users password to a hash.
* @param plaintext The provided user input.
@@ -6,6 +6,7 @@ import {
PasswordCompare,
ReinstateInvite,
ValidateCaptcha,
ValidateEmail,
ValidatePassword,
} from "./auth";
import { Router } from "express";
@@ -37,8 +38,6 @@ const logger = CreateLogCtx(__filename);
const router: Router = Router({ mergeParams: true });
const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
/**
* Logs in a user.
* @name POST /api/v1/auth/login
@@ -170,7 +169,7 @@ router.post(
{
username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
"!password": ValidatePassword,
email: p.regex(LAZY_EMAIL_REGEX),
email: ValidateEmail,
inviteCode: "*string",
captcha: "string",
},
@@ -11,16 +11,19 @@ import notifsRouter from "./notifications/router";
import pfpRouter from "./pfp/router";
import sessionsRouter from "./sessions/router";
import settingsRouter from "./settings/router";
import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth";
import { HashPassword, PasswordCompare, ValidateEmail, ValidatePassword } from "../../auth/auth";
import { Router } from "express";
import db from "external/mongo/db";
import { GetRecentActivity } from "lib/activity/activity";
import { ONE_MONTH } from "lib/constants/time";
import { SendEmail } from "lib/email/client";
import { EmailFormatVerifyEmail } from "lib/email/formats";
import CreateLogCtx from "lib/logger/logger";
import { GetRivalIDs } from "lib/rivals/rivals";
import { ServerConfig } from "lib/setup/config";
import { p } from "prudence";
import prValidate from "server/middleware/prudence-validate";
import { DeleteUndefinedProps, IsNonEmptyString, StripUrl } from "utils/misc";
import { DeleteUndefinedProps, IsNonEmptyString, Random20Hex, StripUrl } from "utils/misc";
import { optNullFluffStrField } from "utils/prudence";
import {
GetGoalSummary,
@@ -29,7 +32,7 @@ import {
GetRecentlyViewedFoldersAnyGPT,
} from "utils/queries/summary";
import { GetUser } from "utils/req-tachi-data";
import { FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user";
import { CheckIfEmailInUse, FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user";
import type {
AnyProfileRatingAlg,
GPTString,
@@ -309,6 +312,120 @@ router.get("/is-email-verified", RequireSelfRequestFromUser, async (req, res) =>
});
});
/**
* Get what email this user signed up with.
*
* @name GET /api/v1/users/:userID/email
*/
router.get("/email", RequireSelfRequestFromUser, async (req, res) => {
const user = GetUser(req);
const email = await db["user-private-information"].findOne({
userID: user.id,
});
if (email) {
return res.status(200).json({
success: true,
description: `User signed up with this email.`,
body: email.email,
});
}
logger.error(`User ${user.id} doesn't have private info?`);
return res.status(500).json({
success: false,
description: `Internal Server Error`,
});
});
/**
* Change what email is associated with this account.
*
* @name GET /api/v1/users/:userID/email
*/
router.post(
"/change-email",
RequireSelfRequestFromUser,
prValidate({
email: ValidateEmail,
"!password": ValidatePassword,
}),
async (req, res) => {
const user = GetUser(req);
const body = req.safeBody as {
"!password": string;
email: string;
};
const privateInfo = await db["user-private-information"].findOne({
userID: user.id,
});
if (!privateInfo) {
logger.error(`User ${user.id} has no associated private info?`);
return res.status(500).json({
success: false,
description: `Internal server error.`,
});
}
const isPasswordValid = await PasswordCompare(body["!password"], privateInfo.password);
if (!isPasswordValid) {
return res.status(403).json({
success: false,
description: `Invalid password.`,
});
}
const existingEmail = await CheckIfEmailInUse(body.email);
if (existingEmail) {
logger.info(`User attempted to change to email that was already in use.`);
return res.status(409).json({
success: false,
description: `This email is already in use.`,
});
}
logger.info(`User ${user.id} changed email from ${privateInfo.email} to ${body.email}`);
await db["user-private-information"].update(
{
userID: user.id,
},
{
$set: {
email: body.email,
},
}
);
if (ServerConfig.EMAIL_CONFIG) {
const resetEmailCode = Random20Hex();
await db["verify-email-codes"].insert({
code: resetEmailCode,
userID: user.id,
email: body.email,
});
const { text, html } = EmailFormatVerifyEmail(user.username, resetEmailCode);
void SendEmail(body.email, "Email Verification", html, text);
}
return res.status(200).json({
success: true,
description: `Re-sent email verification to new email`,
body: null,
});
}
);
/**
* Changes the users password.
* Requires self-key level permissions.