summary refs log tree commit diff
path: root/api/src/routes
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-08-06 10:48:21 +1000
committerGitHub <noreply@github.com>2022-08-06 10:48:21 +1000
commitb1e51d5e6e2913ee6101df343cbde721d3a350e9 (patch)
treef7e0b8e79e08f88842f79fc43655adc5d893d1e2 /api/src/routes
parentCaptcha checking (diff)
parentMerge pull request #815 from fosscord/translation (diff)
downloadserver-b1e51d5e6e2913ee6101df343cbde721d3a350e9.tar.xz
Merge branch 'master' into feat/captchaVerify
Diffstat (limited to 'api/src/routes')
-rw-r--r--api/src/routes/auth/login.ts17
-rw-r--r--api/src/routes/auth/mfa/totp.ts49
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts3
-rw-r--r--api/src/routes/users/@me/index.ts1
-rw-r--r--api/src/routes/users/@me/mfa/codes.ts48
-rw-r--r--api/src/routes/users/@me/mfa/totp/disable.ts45
-rw-r--r--api/src/routes/users/@me/mfa/totp/enable.ts54
-rw-r--r--api/src/routes/users/@me/notes.ts43
8 files changed, 247 insertions, 13 deletions
diff --git a/api/src/routes/auth/login.ts b/api/src/routes/auth/login.ts
index cd373d9d..80e5c4e8 100644
--- a/api/src/routes/auth/login.ts
+++ b/api/src/routes/auth/login.ts
@@ -2,6 +2,7 @@ import { Request, Response, Router } from "express";
 import { route, getIpAdress, verifyCaptcha } from "@fosscord/api";
 import bcrypt from "bcrypt";
 import { Config, User, generateToken, adjustEmail, FieldErrors } from "@fosscord/util";
+import crypto from "crypto";
 
 const router: Router = Router();
 export default router;
@@ -45,7 +46,7 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
 
 	const user = await User.findOneOrFail({
 		where: [{ phone: login }, { email: login }],
-		select: ["data", "id", "disabled", "deleted", "settings"]
+		select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"]
 	}).catch((e) => {
 		throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
 	});
@@ -65,6 +66,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
 		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
 	}
 
+	if (user.mfa_enabled) {
+		// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
+		const ticket = crypto.randomBytes(40).toString("hex");
+
+		await User.update({ id: user.id }, { totp_last_ticket: ticket });
+
+		return res.json({
+			ticket: ticket,
+			mfa: true,
+			sms: false,	// TODO
+			token: null,
+		})
+	}
+
 	const token = await generateToken(user.id);
 
 	// Notice this will have a different token structure, than discord
diff --git a/api/src/routes/auth/mfa/totp.ts b/api/src/routes/auth/mfa/totp.ts
new file mode 100644
index 00000000..cec6e5ee
--- /dev/null
+++ b/api/src/routes/auth/mfa/totp.ts
@@ -0,0 +1,49 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, FieldErrors, generateToken, User } from "@fosscord/util";
+import { verifyToken } from "node-2fa";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+export interface TotpSchema {
+	code: string,
+	ticket: string,
+	gift_code_sku_id?: string | null,
+	login_source?: string | null,
+}
+
+router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => {
+	const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema;
+
+	const user = await User.findOneOrFail({
+		where: {
+			totp_last_ticket: ticket,
+		},
+		select: [
+			"id",
+			"totp_secret",
+			"settings",
+		],
+	});
+
+	const backup = await BackupCode.findOne({ code: code, expired: false, consumed: false, user: { id: user.id }});
+
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+	else {
+		backup.consumed = true;
+		await backup.save();
+	}
+
+	await User.update({ id: user.id }, { totp_last_ticket: "" });
+
+	return res.json({
+		token: await generateToken(user.id),
+		user_settings: user.settings,
+	});
+});
+
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 4ec3df72..be556fb2 100644
--- a/api/src/routes/guilds/#guild_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -7,7 +7,8 @@ import { GuildCreateSchema } from "../index";
 
 const router = Router();
 
-export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
+export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels" | "name"> {
+	name?: string;
 	banner?: string | null;
 	splash?: string | null;
 	description?: string;
diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index 1af413c4..7fc20457 100644
--- a/api/src/routes/users/@me/index.ts
+++ b/api/src/routes/users/@me/index.ts
@@ -11,6 +11,7 @@ export interface UserModifySchema {
 	 * @maxLength 100
 	 */
 	username?: string;
+	discriminator?: string;
 	avatar?: string | null;
 	/**
 	 * @maxLength 1024
diff --git a/api/src/routes/users/@me/mfa/codes.ts b/api/src/routes/users/@me/mfa/codes.ts
new file mode 100644
index 00000000..6ddf32f0
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,48 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, Config, FieldErrors, generateMfaBackupCodes, User } from "@fosscord/util";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+export interface MfaCodesSchema {
+	password: string;
+	regenerate?: boolean;
+}
+
+// TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients
+
+router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => {
+	const { password, regenerate } = req.body as MfaCodesSchema;
+
+	const user = await User.findOneOrFail({ id: req.user_id }, { select: ["data"] });
+
+	if (!await bcrypt.compare(password, user.data.hash || "")) {
+		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+	}
+
+	var codes: BackupCode[];
+	if (regenerate && Config.get().security.twoFactor.generateBackupCodes) {
+		await BackupCode.update(
+			{ user: { id: req.user_id } },
+			{ expired: true }
+		);
+
+		codes = generateMfaBackupCodes(req.user_id);
+		await Promise.all(codes.map(x => x.save()));
+	}
+	else {
+		codes = await BackupCode.find({
+			user: {
+				id: req.user_id,
+			},
+			expired: false,
+		});
+	}
+
+	return res.json({
+		backup_codes: codes.map(x => ({ ...x, expired: undefined })),
+	})
+});
+
+export default router;
diff --git a/api/src/routes/users/@me/mfa/totp/disable.ts b/api/src/routes/users/@me/mfa/totp/disable.ts
new file mode 100644
index 00000000..5e039ea3
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,45 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { verifyToken } from 'node-2fa';
+import { HTTPError } from "lambert-server";
+import { User, generateToken, BackupCode } from "@fosscord/util";
+
+const router = Router();
+
+export interface TotpDisableSchema {
+	code: string;
+}
+
+router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpDisableSchema;
+
+	const user = await User.findOneOrFail({ id: req.user_id }, { select: ["totp_secret"] });
+
+	const backup = await BackupCode.findOne({ code: body.code });
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, body.code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+
+	await User.update(
+		{ id: req.user_id },
+		{
+			mfa_enabled: false,
+			totp_secret: "",
+		},
+	);
+
+	await BackupCode.update(
+		{ user: { id: req.user_id } },
+		{
+			expired: true,
+		}
+	);
+
+	return res.json({
+		token: await generateToken(user.id),
+	});
+});
+
+export default router;
\ No newline at end of file
diff --git a/api/src/routes/users/@me/mfa/totp/enable.ts b/api/src/routes/users/@me/mfa/totp/enable.ts
new file mode 100644
index 00000000..87f36d55
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,54 @@
+import { Router, Request, Response } from "express";
+import { User, generateToken, BackupCode, generateMfaBackupCodes, Config } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "lambert-server";
+import { verifyToken } from 'node-2fa';
+
+const router = Router();
+
+export interface TotpEnableSchema {
+	password: string;
+	code?: string;
+	secret?: string;
+}
+
+router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpEnableSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] });
+
+	// TODO: Are guests allowed to enable 2fa?
+	if (user.data.hash) {
+		if (!await bcrypt.compare(body.password, user.data.hash)) {
+			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+		}
+	}
+
+	if (!body.secret)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005);
+
+	if (!body.code)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	if (verifyToken(body.secret, body.code)?.delta != 0)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	let backup_codes: BackupCode[] = [];
+	if (Config.get().security.twoFactor.generateBackupCodes) {
+		backup_codes = generateMfaBackupCodes(req.user_id);
+		await Promise.all(backup_codes.map(x => x.save()));
+	}
+
+	await User.update(
+		{ id: req.user_id },
+		{ mfa_enabled: true, totp_secret: body.secret }
+	);
+
+	res.send({
+		token: await generateToken(user.id),
+		backup_codes: backup_codes.map(x => ({ ...x, expired: undefined })),
+	});
+});
+
+export default router;
\ No newline at end of file
diff --git a/api/src/routes/users/@me/notes.ts b/api/src/routes/users/@me/notes.ts
index 4887b191..3c503942 100644
--- a/api/src/routes/users/@me/notes.ts
+++ b/api/src/routes/users/@me/notes.ts
@@ -1,37 +1,58 @@
 import { Request, Response, Router } from "express";
 import { route } from "@fosscord/api";
-import { User, emitEvent } from "@fosscord/util";
+import { User, Note, emitEvent, Snowflake } from "@fosscord/util";
 
 const router: Router = Router();
 
 router.get("/:id", route({}), async (req: Request, res: Response) => {
 	const { id } = req.params;
-	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["notes"] });
 
-	const note = user.notes[id];
+	const note = await Note.findOneOrFail({
+		where: {
+			owner: { id: req.user_id },
+			target: { id: id },
+		}
+	});
+
 	return res.json({
-		note: note,
+		note: note?.content,
 		note_user_id: id,
-		user_id: user.id,
+		user_id: req.user_id,
 	});
 });
 
 router.put("/:id", route({}), async (req: Request, res: Response) => {
 	const { id } = req.params;
-	const user = await User.findOneOrFail({ where: { id: req.user_id } });
-	const noteUser = await User.findOneOrFail({ where: { id: id }});		//if noted user does not exist throw
+	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+	const target = await User.findOneOrFail({ where: { id: id } });		//if noted user does not exist throw
 	const { note } = req.body;
 
-	await User.update({ id: req.user_id }, { notes: { ...user.notes, [noteUser.id]: note } });
+	if (note && note.length) {
+		// upsert a note
+		if (await Note.findOne({ owner: { id: owner.id }, target: { id: target.id } })) {
+			Note.update(
+				{ owner: { id: owner.id }, target: { id: target.id } },
+				{ owner, target, content: note }
+			);
+		}
+		else {
+			Note.insert(
+				{ id: Snowflake.generate(), owner, target, content: note }
+			);
+		}
+	}
+	else {
+		await Note.delete({ owner: { id: owner.id }, target: { id: target.id } });
+	}
 
 	await emitEvent({
 		event: "USER_NOTE_UPDATE",
 		data: {
 			note: note,
-			id: noteUser.id
+			id: target.id
 		},
-		user_id: user.id,
-	})
+		user_id: owner.id,
+	});
 
 	return res.status(204);
 });