summary refs log tree commit diff
path: root/src/api/routes/users
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/routes/users')
-rw-r--r--src/api/routes/users/#id/index.ts13
-rw-r--r--src/api/routes/users/#id/profile.ts91
-rw-r--r--src/api/routes/users/#id/relationships.ts41
-rw-r--r--src/api/routes/users/@me/activities/statistics/applications.ts11
-rw-r--r--src/api/routes/users/@me/affinities/guilds.ts11
-rw-r--r--src/api/routes/users/@me/affinities/users.ts11
-rw-r--r--src/api/routes/users/@me/applications/#app_id/entitlements.ts11
-rw-r--r--src/api/routes/users/@me/billing/country-code.ts11
-rw-r--r--src/api/routes/users/@me/billing/payment-sources.ts11
-rw-r--r--src/api/routes/users/@me/billing/subscriptions.ts11
-rw-r--r--src/api/routes/users/@me/channels.ts20
-rw-r--r--src/api/routes/users/@me/connections.ts11
-rw-r--r--src/api/routes/users/@me/delete.ts32
-rw-r--r--src/api/routes/users/@me/devices.ts11
-rw-r--r--src/api/routes/users/@me/disable.ts26
-rw-r--r--src/api/routes/users/@me/email-settings.ts20
-rw-r--r--src/api/routes/users/@me/entitlements.ts11
-rw-r--r--src/api/routes/users/@me/guilds.ts57
-rw-r--r--src/api/routes/users/@me/guilds/premium/subscription-slots.ts11
-rw-r--r--src/api/routes/users/@me/index.ts72
-rw-r--r--src/api/routes/users/@me/library.ts11
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts45
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts41
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts48
-rw-r--r--src/api/routes/users/@me/notes.ts60
-rw-r--r--src/api/routes/users/@me/relationships.ts204
-rw-r--r--src/api/routes/users/@me/settings.ts18
27 files changed, 920 insertions, 0 deletions
diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
new file mode 100644
index 00000000..bdb1060f
--- /dev/null
+++ b/src/api/routes/users/#id/index.ts
@@ -0,0 +1,13 @@
+import { Router, Request, Response } from "express";
+import { User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+
+	res.json(await User.getPublicUser(id));
+});
+
+export default router;
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
new file mode 100644
index 00000000..fafa6b93
--- /dev/null
+++ b/src/api/routes/users/#id/profile.ts
@@ -0,0 +1,91 @@
+import { Router, Request, Response } from "express";
+import { PublicConnectedAccount, PublicUser, User, UserPublic, Member, Guild } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+export interface UserProfileResponse {
+	user: UserPublic;
+	connected_accounts: PublicConnectedAccount;
+	premium_guild_since?: Date;
+	premium_since?: Date;
+}
+
+router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => {
+	if (req.params.id === "@me") req.params.id = req.user_id;
+
+	const { guild_id, with_mutual_guilds } = req.query;
+
+	const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] });
+
+	var mutual_guilds: object[] = [];
+	var premium_guild_since;
+
+	if (with_mutual_guilds == "true") {
+		const requested_member = await Member.find({ where: { id: req.params.id } });
+		const self_member = await Member.find({ where: { id: req.user_id } });
+
+		for (const rmem of requested_member) {
+			if (rmem.premium_since) {
+				if (premium_guild_since) {
+					if (premium_guild_since > rmem.premium_since) {
+						premium_guild_since = rmem.premium_since;
+					}
+				} else {
+					premium_guild_since = rmem.premium_since;
+				}
+			}
+			for (const smem of self_member) {
+				if (smem.guild_id === rmem.guild_id) {
+					mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick });
+				}
+			}
+		}
+	}
+
+	const guild_member = guild_id && typeof guild_id == "string"
+		? await Member.findOneOrFail({ id: req.params.id, guild_id: guild_id }, { relations: ["roles"] })
+		: undefined;
+
+	// TODO: make proper DTO's in util?
+
+	const userDto = {
+		username: user.username,
+		discriminator: user.discriminator,
+		id: user.id,
+		public_flags: user.public_flags,
+		avatar: user.avatar,
+		accent_color: user.accent_color,
+		banner: user.banner,
+		bio: req.user_bot ? null : user.bio,
+		bot: user.bot
+	};
+
+	const guildMemberDto = guild_member ? {
+		avatar: user.avatar,	// TODO
+		banner: user.banner,	// TODO
+		bio: req.user_bot ? null : user.bio, // TODO
+		communication_disabled_until: null,	// TODO
+		deaf: guild_member.deaf,
+		flags: user.flags,
+		is_pending: guild_member.pending,
+		pending: guild_member.pending,	// why is this here twice, discord?
+		joined_at: guild_member.joined_at,
+		mute: guild_member.mute,
+		nick: guild_member.nick,
+		premium_since: guild_member.premium_since,
+		roles: guild_member.roles.map(x => x.id).filter(id => id != guild_id),
+		user: userDto
+	} : undefined;
+
+	res.json({
+		connected_accounts: user.connected_accounts,
+		premium_guild_since: premium_guild_since, // TODO
+		premium_since: user.premium_since, // TODO
+		mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
+		user: userDto,
+		guild_member: guildMemberDto,
+	});
+});
+
+export default router;
diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts
new file mode 100644
index 00000000..61655c25
--- /dev/null
+++ b/src/api/routes/users/#id/relationships.ts
@@ -0,0 +1,41 @@
+import { Router, Request, Response } from "express";
+import { User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+export interface UserRelationsResponse {
+	object: {
+		id?: string,
+		username?: string,
+		avatar?: string, 
+		discriminator?: string, 
+		public_flags?: number
+	}
+}
+
+
+router.get("/", route({ test: { response: { body: "UserRelationsResponse" } } }), async (req: Request, res: Response) => {
+	let mutual_relations: object[] = [];
+    const requested_relations = await User.findOneOrFail({
+		where: { id: req.params.id },
+		relations: ["relationships"]
+	});
+    const self_relations = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships"]
+	});
+	
+    for(const rmem of requested_relations.relationships) {
+		for(const smem of self_relations.relationships)
+		if (rmem.to_id === smem.to_id && rmem.type === 1 && rmem.to_id !== req.user_id) {
+			let relation_user = await User.getPublicUser(rmem.to_id)
+
+			mutual_relations.push({id: relation_user.id, username: relation_user.username, avatar: relation_user.avatar, discriminator: relation_user.discriminator, public_flags: relation_user.public_flags})
+		}
+	}
+
+	res.json(mutual_relations)
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/activities/statistics/applications.ts b/src/api/routes/users/@me/activities/statistics/applications.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/activities/statistics/applications.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/affinities/guilds.ts b/src/api/routes/users/@me/affinities/guilds.ts
new file mode 100644
index 00000000..8d744744
--- /dev/null
+++ b/src/api/routes/users/@me/affinities/guilds.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send({ guild_affinities: [] });
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/affinities/users.ts b/src/api/routes/users/@me/affinities/users.ts
new file mode 100644
index 00000000..6d4e4991
--- /dev/null
+++ b/src/api/routes/users/@me/affinities/users.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send({ user_affinities: [], inverse_user_affinities: [] });
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/applications/#app_id/entitlements.ts b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/country-code.ts b/src/api/routes/users/@me/billing/country-code.ts
new file mode 100644
index 00000000..33d40796
--- /dev/null
+++ b/src/api/routes/users/@me/billing/country-code.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json({ country_code: "US" }).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/payment-sources.ts b/src/api/routes/users/@me/billing/payment-sources.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/billing/payment-sources.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/subscriptions.ts b/src/api/routes/users/@me/billing/subscriptions.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/billing/subscriptions.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts
new file mode 100644
index 00000000..ad483529
--- /dev/null
+++ b/src/api/routes/users/@me/channels.ts
@@ -0,0 +1,20 @@
+import { Request, Response, Router } from "express";
+import { Recipient, DmChannelDTO, Channel, DmChannelCreateSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const recipients = await Recipient.find({
+		where: { user_id: req.user_id, closed: false },
+		relations: ["channel", "channel.recipients"]
+	});
+	res.json(await Promise.all(recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id]))));
+});
+
+router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as DmChannelCreateSchema;
+	res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name));
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/connections.ts b/src/api/routes/users/@me/connections.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/connections.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts
new file mode 100644
index 00000000..1d81c2b9
--- /dev/null
+++ b/src/api/routes/users/@me/delete.ts
@@ -0,0 +1,32 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "@fosscord/util";
+
+const router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object
+	let correctpass = true;
+
+	if (user.data.hash) {
+		// guest accounts can delete accounts without password
+		correctpass = await bcrypt.compare(req.body.password, user.data.hash);
+		if (!correctpass) {
+			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+		}
+	}
+
+	// TODO: decrement guild member count
+
+	if (correctpass) {
+		await Promise.all([User.delete({ id: req.user_id }), Member.delete({ id: req.user_id })]);
+
+		res.sendStatus(204);
+	} else {
+		res.sendStatus(401);
+	}
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/devices.ts b/src/api/routes/users/@me/devices.ts
new file mode 100644
index 00000000..8556a3ad
--- /dev/null
+++ b/src/api/routes/users/@me/devices.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.post("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts
new file mode 100644
index 00000000..4aff3774
--- /dev/null
+++ b/src/api/routes/users/@me/disable.ts
@@ -0,0 +1,26 @@
+import { User } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object
+	let correctpass = true;
+
+	if (user.data.hash) {
+		// guest accounts can delete accounts without password
+		correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/
+	}
+
+	if (correctpass) {
+		await User.update({ id: req.user_id }, { disabled: true });
+
+		res.sendStatus(204);
+	} else {
+		res.status(400).json({ message: "Password does not match", code: 50018 });
+	}
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/email-settings.ts b/src/api/routes/users/@me/email-settings.ts
new file mode 100644
index 00000000..3114984e
--- /dev/null
+++ b/src/api/routes/users/@me/email-settings.ts
@@ -0,0 +1,20 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json({
+		categories: {
+			social: true,
+			communication: true,
+			tips: false,
+			updates_and_announcements: false,
+			recommendations_and_events: false
+		},
+		initialized: false
+	}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/entitlements.ts b/src/api/routes/users/@me/entitlements.ts
new file mode 100644
index 00000000..341e2b4c
--- /dev/null
+++ b/src/api/routes/users/@me/entitlements.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/gifts", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts
new file mode 100644
index 00000000..4d4fccd4
--- /dev/null
+++ b/src/api/routes/users/@me/guilds.ts
@@ -0,0 +1,57 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent, Config } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } });
+
+	let guild = members.map((x) => x.guild);
+
+	if ("with_counts" in req.query && req.query.with_counts == "true") {
+		guild = []; // TODO: Load guilds with user role permissions number
+	}
+
+	res.json(guild);
+});
+
+// user send to leave a certain guild
+router.delete("/:guild_id", route({}), async (req: Request, res: Response) => {
+	const { autoJoin } = Config.get().guild;
+	const { guild_id } = req.params;
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] });
+
+	if (!guild) throw new HTTPError("Guild doesn't exist", 404);
+	if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400);
+	if (autoJoin.enabled && autoJoin.guilds.includes(guild_id) && !autoJoin.canLeave) {
+		throw new HTTPError("You can't leave instance auto join guilds", 400);
+	}
+
+	await Promise.all([
+		Member.delete({ id: req.user_id, guild_id: guild_id }),
+		emitEvent({
+			event: "GUILD_DELETE",
+			data: {
+				id: guild_id
+			},
+			user_id: req.user_id
+		} as GuildDeleteEvent)
+	]);
+
+	const user = await User.getPublicUser(req.user_id);
+
+	await emitEvent({
+		event: "GUILD_MEMBER_REMOVE",
+		data: {
+			guild_id: guild_id,
+			user: user
+		},
+		guild_id: guild_id
+	} as GuildMemberRemoveEvent);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/guilds/premium/subscription-slots.ts b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
new file mode 100644
index 00000000..7d095451
--- /dev/null
+++ b/src/api/routes/users/@me/index.ts
@@ -0,0 +1,72 @@
+import { Router, Request, Response } from "express";
+import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors, UserModifySchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { OrmUtils, generateToken } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } }));
+});
+
+router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => {
+	var token = null as any;
+	const body = req.body as UserModifySchema;
+
+	if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
+	if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
+	let user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] });
+
+	if (body.password) {
+		if (user.data?.hash) {
+			const same_password = await bcrypt.compare(body.password, user.data.hash || "");
+			if (!same_password) {
+				throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+			}
+		} else {
+			user.data.hash = await bcrypt.hash(body.password, 12);
+		}
+	}
+
+	if (body.new_password) {
+		if (!body.password && !user.email) {
+			throw FieldErrors({
+				password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+			});
+		}
+		user.data.hash = await bcrypt.hash(body.new_password, 12);
+		user.data.valid_tokens_since = new Date();
+		token = await generateToken(user.id) as string;
+	}
+
+    if(body.username){
+        let check_username = body?.username?.replace(/\s/g, '');
+        if(!check_username) {
+            throw FieldErrors({
+                username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+            });
+        }
+    }
+
+	user = OrmUtils.mergeDeep(user, body);
+	await user.save();
+
+	// @ts-ignore
+	delete user.data;
+
+	// TODO: send update member list event in gateway
+	await emitEvent({
+		event: "USER_UPDATE",
+		user_id: req.user_id,
+		data: user
+	} as UserUpdateEvent);
+	
+	res.json({
+		...user,
+		token
+	});
+});
+
+export default router;
+// {"message": "Invalid two-factor code", "code": 60008}
diff --git a/src/api/routes/users/@me/library.ts b/src/api/routes/users/@me/library.ts
new file mode 100644
index 00000000..7ac13bae
--- /dev/null
+++ b/src/api/routes/users/@me/library.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send([]);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts
new file mode 100644
index 00000000..4224a1c0
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,45 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, Config, FieldErrors, generateMfaBackupCodes, MfaCodesSchema, User } from "@fosscord/util";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+// 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({ where: { 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({
+			where: {
+				user: {
+					id: req.user_id,
+				},
+				expired: false
+			}
+		});
+	}
+
+	return res.json({
+		backup_codes: codes.map(x => ({ ...x, expired: undefined })),
+	})
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
new file mode 100644
index 00000000..2fe9355c
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,41 @@
+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, TotpDisableSchema } from "@fosscord/util";
+
+const router = Router();
+
+router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpDisableSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["totp_secret"] });
+
+	const backup = await BackupCode.findOne({ where: { 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/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
new file mode 100644
index 00000000..ac668d1d
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,48 @@
+import { Router, Request, Response } from "express";
+import { User, generateToken, BackupCode, generateMfaBackupCodes, Config, TotpEnableSchema } 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();
+
+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/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts
new file mode 100644
index 00000000..f938f088
--- /dev/null
+++ b/src/api/routes/users/@me/notes.ts
@@ -0,0 +1,60 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+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 note = await Note.findOneOrFail({
+		where: {
+			owner: { id: req.user_id },
+			target: { id: id },
+		}
+	});
+
+	return res.json({
+		note: note?.content,
+		note_user_id: id,
+		user_id: req.user_id,
+	});
+});
+
+router.put("/:id", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+	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;
+
+	if (note && note.length) {
+		// upsert a note
+		if (await Note.findOne({ where: { 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: target.id
+		},
+		user_id: owner.id,
+	});
+
+	return res.status(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
new file mode 100644
index 00000000..f7464b99
--- /dev/null
+++ b/src/api/routes/users/@me/relationships.ts
@@ -0,0 +1,204 @@
+import {
+	RelationshipAddEvent,
+	User,
+	PublicUserProjection,
+	RelationshipType,
+	RelationshipRemoveEvent,
+	emitEvent,
+	Relationship,
+	Config
+} from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "@fosscord/util";
+import { DiscordApiErrors } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection];
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships", "relationships.to"],
+		select: ["relationships"]
+	});
+
+	//TODO DTO
+	const related_users = user.relationships.map((r) => {
+		return {
+			id: r.to.id,
+			type: r.type,
+			nickname: null,
+			user: r.to.toPublicUser()
+		};
+	});
+
+	return res.json(related_users);
+});
+
+router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => {
+	return await updateRelationship(
+		req,
+		res,
+		await User.findOneOrFail({ where: { id: req.params.id }, relations: ["relationships", "relationships.to"], select: userProjection }),
+		req.body.type ?? RelationshipType.friends
+	);
+});
+
+router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => {
+	return await updateRelationship(
+		req,
+		res,
+		await User.findOneOrFail({
+			relations: ["relationships", "relationships.to"],
+			select: userProjection,
+			where: {
+				discriminator: String(req.body.discriminator).padStart(4, "0"), //Discord send the discriminator as integer, we need to add leading zeroes
+				username: req.body.username
+			}
+		}),
+		req.body.type
+	);
+});
+
+router.delete("/:id", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend");
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] });
+	const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] });
+
+	const relationship = user.relationships.find((x) => x.to_id === id);
+	const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id);
+
+	if (!relationship) throw new HTTPError("You are not friends with the user", 404);
+	if (relationship?.type === RelationshipType.blocked) {
+		// unblock user
+
+		await Promise.all([
+			Relationship.delete({ id: relationship.id }),
+			emitEvent({
+				event: "RELATIONSHIP_REMOVE",
+				user_id: req.user_id,
+				data: relationship.toPublicRelationship()
+			} as RelationshipRemoveEvent)
+		]);
+		return res.sendStatus(204);
+	}
+	if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+		await Promise.all([
+			Relationship.delete({ id: friendRequest.id }),
+			await emitEvent({
+				event: "RELATIONSHIP_REMOVE",
+				data: friendRequest.toPublicRelationship(),
+				user_id: id
+			} as RelationshipRemoveEvent)
+		]);
+	}
+
+	await Promise.all([
+		Relationship.delete({ id: relationship.id }),
+		emitEvent({
+			event: "RELATIONSHIP_REMOVE",
+			data: relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipRemoveEvent)
+	]);
+
+	return res.sendStatus(204);
+});
+
+export default router;
+
+async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) {
+	const id = friend.id;
+	if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
+
+	const user = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships", "relationships.to"],
+		select: userProjection
+	});
+
+	let relationship = user.relationships.find((x) => x.to_id === id);
+	const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id);
+
+	// TODO: you can add infinitely many blocked users (should this be prevented?)
+	if (type === RelationshipType.blocked) {
+		if (relationship) {
+			if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user");
+			relationship.type = RelationshipType.blocked;
+			await relationship.save();
+		} else {
+			relationship = await (OrmUtils.mergeDeep(new Relationship(), { to_id: id, type: RelationshipType.blocked, from_id: req.user_id }) as Relationship).save();
+		}
+
+		if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+			await Promise.all([
+				Relationship.delete({ id: friendRequest.id }),
+				emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					data: friendRequest.toPublicRelationship(),
+					user_id: id
+				} as RelationshipRemoveEvent)
+			]);
+		}
+
+		await emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipAddEvent);
+
+		return res.sendStatus(204);
+	}
+
+	const { maxFriends } = Config.get().limits.user;
+	if (user.relationships.length >= maxFriends) throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends);
+
+	let incoming_relationship = OrmUtils.mergeDeep(new Relationship(), { nickname: undefined, type: RelationshipType.incoming, to: user, from: friend });
+	let outgoing_relationship = OrmUtils.mergeDeep(new Relationship(), {
+		nickname: undefined,
+		type: RelationshipType.outgoing,
+		to: friend,
+		from: user
+	});
+
+	if (friendRequest) {
+		if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
+		if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		// accept friend request
+		incoming_relationship = friendRequest as any; //TODO: checkme, any cast
+		incoming_relationship.type = RelationshipType.friends;
+	}
+
+	if (relationship) {
+		if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request");
+		if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request");
+		if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		outgoing_relationship = relationship as any; //TODO: checkme, any cast
+		outgoing_relationship.type = RelationshipType.friends;
+	}
+
+	await Promise.all([
+		incoming_relationship.save(),
+		outgoing_relationship.save(),
+		emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: outgoing_relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipAddEvent),
+		emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: {
+				...incoming_relationship.toPublicRelationship(),
+				should_notify: true
+			},
+			user_id: id
+		} as RelationshipAddEvent)
+	]);
+
+	return res.sendStatus(204);
+}
diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts
new file mode 100644
index 00000000..7578d36e
--- /dev/null
+++ b/src/api/routes/users/@me/settings.ts
@@ -0,0 +1,18 @@
+import { Router, Response, Request } from "express";
+import { User, UserSettings } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as UserSettings;
+	if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id, bot: false }, relations: ["settings"] });
+	user.settings = { ...user.settings, ...body } as UserSettings;
+	await user.save();
+
+	res.sendStatus(204);
+});
+
+export default router;