summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorPuyodead1 <puyodead@protonmail.com>2023-02-24 01:54:10 -0500
committerPuyodead1 <puyodead@protonmail.com>2023-02-24 01:54:10 -0500
commit05453ec14880732c5d0d20fd3575bb2b3952760d (patch)
treec3b7272d7eafff2e988702b9bf3b93f24a101881 /src/api
parentadd SendGrid transport (diff)
downloadserver-05453ec14880732c5d0d20fd3575bb2b3952760d.tar.xz
implement password reset
Diffstat (limited to 'src/api')
-rw-r--r--src/api/middlewares/Authentication.ts2
-rw-r--r--src/api/routes/auth/forgot.ts92
-rw-r--r--src/api/routes/auth/reset.ts57
-rw-r--r--src/api/routes/auth/verify/resend.ts2
4 files changed, 152 insertions, 1 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index f4c33963..771f0de8 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -29,6 +29,8 @@ export const NO_AUTHORIZATION_ROUTES = [
 	"/auth/mfa/totp",
 	"/auth/mfa/webauthn",
 	"/auth/verify",
+	"/auth/forgot",
+	"/auth/reset",
 	// Routes with a seperate auth system
 	"/webhooks/",
 	// Public information endpoints
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
new file mode 100644
index 00000000..faa43dbb
--- /dev/null
+++ b/src/api/routes/auth/forgot.ts
@@ -0,0 +1,92 @@
+import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
+import {
+	Config,
+	Email,
+	FieldErrors,
+	ForgotPasswordSchema,
+	User,
+} from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post(
+	"/",
+	route({ body: "ForgotPasswordSchema" }),
+	async (req: Request, res: Response) => {
+		const { login, captcha_key } = req.body as ForgotPasswordSchema;
+
+		const config = Config.get();
+
+		if (
+			config.password_reset.requireCaptcha &&
+			config.security.captcha.enabled
+		) {
+			const { sitekey, service } = config.security.captcha;
+			if (!captcha_key) {
+				return res.status(400).json({
+					captcha_key: ["captcha-required"],
+					captcha_sitekey: sitekey,
+					captcha_service: service,
+				});
+			}
+
+			const ip = getIpAdress(req);
+			const verify = await verifyCaptcha(captcha_key, ip);
+			if (!verify.success) {
+				return res.status(400).json({
+					captcha_key: verify["error-codes"],
+					captcha_sitekey: sitekey,
+					captcha_service: service,
+				});
+			}
+		}
+
+		const user = await User.findOneOrFail({
+			where: [{ phone: login }, { email: login }],
+			select: ["username", "id", "disabled", "deleted", "email"],
+			relations: ["security_keys"],
+		}).catch(() => {
+			throw FieldErrors({
+				login: {
+					message: req.t("auth:password_reset.EMAIL_DOES_NOT_EXIST"),
+					code: "EMAIL_DOES_NOT_EXIST",
+				},
+			});
+		});
+
+		if (!user.email)
+			throw FieldErrors({
+				login: {
+					message:
+						"This account does not have an email address associated with it.",
+					code: "NO_EMAIL",
+				},
+			});
+
+		if (user.deleted)
+			return res.status(400).json({
+				message: "This account is scheduled for deletion.",
+				code: 20011,
+			});
+
+		if (user.disabled)
+			return res.status(400).json({
+				message: req.t("auth:login.ACCOUNT_DISABLED"),
+				code: 20013,
+			});
+
+		return await Email.sendResetPassword(user, user.email)
+			.then(() => {
+				return res.sendStatus(204);
+			})
+			.catch((e) => {
+				console.error(
+					`Failed to send password reset email to ${user.username}#${user.discriminator}: ${e}`,
+				);
+				throw new HTTPError("Failed to send password reset email", 500);
+			});
+	},
+);
+
+export default router;
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
new file mode 100644
index 00000000..94053e1a
--- /dev/null
+++ b/src/api/routes/auth/reset.ts
@@ -0,0 +1,57 @@
+import { route } from "@fosscord/api";
+import {
+	checkToken,
+	Config,
+	Email,
+	FieldErrors,
+	generateToken,
+	PasswordResetSchema,
+	User,
+} from "@fosscord/util";
+import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+
+const router = Router();
+
+router.post(
+	"/",
+	route({ body: "PasswordResetSchema" }),
+	async (req: Request, res: Response) => {
+		const { password, token } = req.body as PasswordResetSchema;
+
+		try {
+			const { jwtSecret } = Config.get().security;
+			const { user } = await checkToken(token, jwtSecret, true);
+
+			// the salt is saved in the password refer to bcrypt docs
+			const hash = await bcrypt.hash(password, 12);
+
+			const data = {
+				data: {
+					hash,
+					valid_tokens_since: new Date(),
+				},
+			};
+			await User.update({ id: user.id }, data);
+
+			// come on, the user has to have an email to reset their password in the first place
+			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+			await Email.sendPasswordChanged(user, user.email!);
+
+			res.json({ token: await generateToken(user.id) });
+		} catch (e) {
+			if ((e as Error).toString() === "Invalid Token")
+				throw FieldErrors({
+					password: {
+						message: req.t("auth:password_reset.INVALID_TOKEN"),
+						code: "INVALID_TOKEN",
+					},
+				});
+
+			throw new HTTPError((e as Error).toString(), 400);
+		}
+	},
+);
+
+export default router;
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
index 1cd14f23..918af9a1 100644
--- a/src/api/routes/auth/verify/resend.ts
+++ b/src/api/routes/auth/verify/resend.ts
@@ -36,7 +36,7 @@ router.post(
 			throw new HTTPError("User does not have an email address", 400);
 		}
 
-		await Email.sendVerificationEmail(user, user.email)
+		await Email.sendVerifyEmail(user, user.email)
 			.then(() => {
 				return res.sendStatus(204);
 			})