mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 08:48:15 +03:00
feat: email changing
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user