summary refs log tree commit diff
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-12-24 18:55:14 +1100
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-12-24 18:55:14 +1100
commit91010235b92ad4bf16f0377a70dc3b862641b8ee (patch)
treed46ac46b7d4762465fe41d642228c411a17a70f9
parentFix creating bot accounts (diff)
downloadserver-91010235b92ad4bf16f0377a70dc3b862641b8ee.tar.xz
OAuth2 authorize bot flow
-rw-r--r--assets/preload-plugins/oauth2.js9
-rw-r--r--assets/schemas.json580
-rw-r--r--src/api/routes/oauth2/authorize.ts146
-rw-r--r--src/util/schemas/ApplicationAuthorizeSchema.ts7
-rw-r--r--src/util/schemas/index.ts3
5 files changed, 744 insertions, 1 deletions
diff --git a/assets/preload-plugins/oauth2.js b/assets/preload-plugins/oauth2.js
new file mode 100644
index 00000000..5b78ec83
--- /dev/null
+++ b/assets/preload-plugins/oauth2.js
@@ -0,0 +1,9 @@
+// Fixes /oauth2 endpoints not requesting a CSS file
+
+if (location.pathname.startsWith("/oauth2/")) {
+	const link = document.createElement("link");
+	link.rel = "stylesheet"
+	link.type = "text/css"
+	link.href = "/assets/40532.f7b1e10347ef10e790ac.css"
+	document.head.appendChild(link)
+}
\ No newline at end of file
diff --git a/assets/schemas.json b/assets/schemas.json
index 874c4f88..879d202e 100644
--- a/assets/schemas.json
+++ b/assets/schemas.json
@@ -26795,6 +26795,586 @@
         },
         "$schema": "http://json-schema.org/draft-07/schema#"
     },
+    "ApplicationAuthorizeSchema": {
+        "type": "object",
+        "properties": {
+            "authorize": {
+                "type": "boolean"
+            },
+            "guild_id": {
+                "type": "string"
+            },
+            "permissions": {
+                "type": "string"
+            },
+            "captcha_key": {
+                "type": "string"
+            },
+            "code": {
+                "type": "string"
+            }
+        },
+        "additionalProperties": false,
+        "required": [
+            "authorize",
+            "guild_id",
+            "permissions"
+        ],
+        "definitions": {
+            "ChannelPermissionOverwriteType": {
+                "enum": [
+                    0,
+                    1,
+                    2
+                ],
+                "type": "number"
+            },
+            "Embed": {
+                "type": "object",
+                "properties": {
+                    "title": {
+                        "type": "string"
+                    },
+                    "type": {
+                        "enum": [
+                            "article",
+                            "gifv",
+                            "image",
+                            "link",
+                            "rich",
+                            "video"
+                        ],
+                        "type": "string"
+                    },
+                    "description": {
+                        "type": "string"
+                    },
+                    "url": {
+                        "type": "string"
+                    },
+                    "timestamp": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "color": {
+                        "type": "integer"
+                    },
+                    "footer": {
+                        "type": "object",
+                        "properties": {
+                            "text": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false,
+                        "required": [
+                            "text"
+                        ]
+                    },
+                    "image": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "thumbnail": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "video": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "provider": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "author": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "fields": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "name": {
+                                    "type": "string"
+                                },
+                                "value": {
+                                    "type": "string"
+                                },
+                                "inline": {
+                                    "type": "boolean"
+                                }
+                            },
+                            "additionalProperties": false,
+                            "required": [
+                                "name",
+                                "value"
+                            ]
+                        }
+                    }
+                },
+                "additionalProperties": false
+            },
+            "EmbedImage": {
+                "type": "object",
+                "properties": {
+                    "url": {
+                        "type": "string"
+                    },
+                    "proxy_url": {
+                        "type": "string"
+                    },
+                    "height": {
+                        "type": "integer"
+                    },
+                    "width": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false
+            },
+            "ChannelModifySchema": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "maxLength": 100,
+                        "type": "string"
+                    },
+                    "type": {
+                        "enum": [
+                            0,
+                            1,
+                            10,
+                            11,
+                            12,
+                            13,
+                            14,
+                            15,
+                            2,
+                            255,
+                            3,
+                            33,
+                            34,
+                            35,
+                            4,
+                            5,
+                            6,
+                            64,
+                            7,
+                            8,
+                            9
+                        ],
+                        "type": "number"
+                    },
+                    "topic": {
+                        "type": "string"
+                    },
+                    "icon": {
+                        "type": [
+                            "null",
+                            "string"
+                        ]
+                    },
+                    "bitrate": {
+                        "type": "integer"
+                    },
+                    "user_limit": {
+                        "type": "integer"
+                    },
+                    "rate_limit_per_user": {
+                        "type": "integer"
+                    },
+                    "position": {
+                        "type": "integer"
+                    },
+                    "permission_overwrites": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string"
+                                },
+                                "type": {
+                                    "$ref": "#/definitions/ChannelPermissionOverwriteType"
+                                },
+                                "allow": {
+                                    "type": "string"
+                                },
+                                "deny": {
+                                    "type": "string"
+                                }
+                            },
+                            "additionalProperties": false,
+                            "required": [
+                                "allow",
+                                "deny",
+                                "id",
+                                "type"
+                            ]
+                        }
+                    },
+                    "parent_id": {
+                        "type": "string"
+                    },
+                    "id": {
+                        "type": "string"
+                    },
+                    "nsfw": {
+                        "type": "boolean"
+                    },
+                    "rtc_region": {
+                        "type": "string"
+                    },
+                    "default_auto_archive_duration": {
+                        "type": "integer"
+                    },
+                    "default_reaction_emoji": {
+                        "type": [
+                            "null",
+                            "string"
+                        ]
+                    },
+                    "flags": {
+                        "type": "integer"
+                    },
+                    "default_thread_rate_limit_per_user": {
+                        "type": "integer"
+                    },
+                    "video_quality_mode": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false
+            },
+            "ActivitySchema": {
+                "type": "object",
+                "properties": {
+                    "afk": {
+                        "type": "boolean"
+                    },
+                    "status": {
+                        "$ref": "#/definitions/Status"
+                    },
+                    "activities": {
+                        "type": "array",
+                        "items": {
+                            "$ref": "#/definitions/Activity"
+                        }
+                    },
+                    "since": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "status"
+                ]
+            },
+            "Status": {
+                "enum": [
+                    "dnd",
+                    "idle",
+                    "invisible",
+                    "offline",
+                    "online"
+                ],
+                "type": "string"
+            },
+            "Activity": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "type": "string"
+                    },
+                    "type": {
+                        "$ref": "#/definitions/ActivityType"
+                    },
+                    "url": {
+                        "type": "string"
+                    },
+                    "created_at": {
+                        "type": "integer"
+                    },
+                    "timestamps": {
+                        "type": "object",
+                        "properties": {
+                            "start": {
+                                "type": "integer"
+                            },
+                            "end": {
+                                "type": "integer"
+                            }
+                        },
+                        "additionalProperties": false,
+                        "required": [
+                            "end",
+                            "start"
+                        ]
+                    },
+                    "application_id": {
+                        "type": "string"
+                    },
+                    "details": {
+                        "type": "string"
+                    },
+                    "state": {
+                        "type": "string"
+                    },
+                    "emoji": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "id": {
+                                "type": "string"
+                            },
+                            "animated": {
+                                "type": "boolean"
+                            }
+                        },
+                        "additionalProperties": false,
+                        "required": [
+                            "animated",
+                            "name"
+                        ]
+                    },
+                    "party": {
+                        "type": "object",
+                        "properties": {
+                            "id": {
+                                "type": "string"
+                            },
+                            "size": {
+                                "type": "array",
+                                "items": [
+                                    {
+                                        "type": "integer"
+                                    }
+                                ],
+                                "minItems": 1,
+                                "maxItems": 1
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "assets": {
+                        "type": "object",
+                        "properties": {
+                            "large_image": {
+                                "type": "string"
+                            },
+                            "large_text": {
+                                "type": "string"
+                            },
+                            "small_image": {
+                                "type": "string"
+                            },
+                            "small_text": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "secrets": {
+                        "type": "object",
+                        "properties": {
+                            "join": {
+                                "type": "string"
+                            },
+                            "spectate": {
+                                "type": "string"
+                            },
+                            "match": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "instance": {
+                        "type": "boolean"
+                    },
+                    "flags": {
+                        "type": "string"
+                    },
+                    "id": {
+                        "type": "string"
+                    },
+                    "sync_id": {
+                        "type": "string"
+                    },
+                    "metadata": {
+                        "type": "object",
+                        "properties": {
+                            "context_uri": {
+                                "type": "string"
+                            },
+                            "album_id": {
+                                "type": "string"
+                            },
+                            "artist_ids": {
+                                "type": "array",
+                                "items": {
+                                    "type": "string"
+                                }
+                            }
+                        },
+                        "additionalProperties": false,
+                        "required": [
+                            "album_id",
+                            "artist_ids"
+                        ]
+                    },
+                    "session_id": {
+                        "type": "string"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "flags",
+                    "name",
+                    "session_id",
+                    "type"
+                ]
+            },
+            "ActivityType": {
+                "enum": [
+                    0,
+                    1,
+                    2,
+                    4,
+                    5
+                ],
+                "type": "number"
+            },
+            "Record<string,[number,number][]>": {
+                "type": "object",
+                "additionalProperties": false
+            },
+            "CustomStatus": {
+                "type": "object",
+                "properties": {
+                    "emoji_id": {
+                        "type": "string"
+                    },
+                    "emoji_name": {
+                        "type": "string"
+                    },
+                    "expires_at": {
+                        "type": "integer"
+                    },
+                    "text": {
+                        "type": "string"
+                    }
+                },
+                "additionalProperties": false
+            },
+            "FriendSourceFlags": {
+                "type": "object",
+                "properties": {
+                    "all": {
+                        "type": "boolean"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "all"
+                ]
+            },
+            "GuildFolder": {
+                "type": "object",
+                "properties": {
+                    "color": {
+                        "type": "integer"
+                    },
+                    "guild_ids": {
+                        "type": "array",
+                        "items": {
+                            "type": "string"
+                        }
+                    },
+                    "id": {
+                        "type": "integer"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "color",
+                    "guild_ids",
+                    "id",
+                    "name"
+                ]
+            },
+            "Partial<ChannelOverride>": {
+                "type": "object",
+                "properties": {
+                    "message_notifications": {
+                        "type": "integer"
+                    },
+                    "mute_config": {
+                        "$ref": "#/definitions/MuteConfig"
+                    },
+                    "muted": {
+                        "type": "boolean"
+                    },
+                    "channel_id": {
+                        "type": [
+                            "null",
+                            "string"
+                        ]
+                    }
+                },
+                "additionalProperties": false
+            },
+            "MuteConfig": {
+                "type": "object",
+                "properties": {
+                    "end_time": {
+                        "type": "integer"
+                    },
+                    "selected_time_window": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "end_time",
+                    "selected_time_window"
+                ]
+            }
+        },
+        "$schema": "http://json-schema.org/draft-07/schema#"
+    },
     "ActivitySchema": {
         "$ref": "#/definitions/ActivitySchema",
         "definitions": {
diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts
new file mode 100644
index 00000000..e4c2e986
--- /dev/null
+++ b/src/api/routes/oauth2/authorize.ts
@@ -0,0 +1,146 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { ApiError, Application, ApplicationAuthorizeSchema, getPermission, DiscordApiErrors, Member, Permissions, User, getRights, Rights, MemberPrivateProjection } from "@fosscord/util";
+const router = Router();
+
+// TODO: scopes, other oauth types
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const {
+		client_id,
+		scope,
+		response_type,
+		redirect_url,
+	} = req.query;
+
+	const app = await Application.findOne({
+		where: {
+			id: client_id as string,
+		},
+		relations: ["bot"],
+	});
+
+	// TODO: use DiscordApiErrors
+	// findOneOrFail throws code 404
+	if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
+	if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
+
+	const bot = app.bot;
+	delete app.bot;
+
+	const user = await User.findOneOrFail({
+		where: {
+			id: req.user_id,
+			bot: false,
+		},
+		select: ["id", "username", "avatar", "discriminator", "public_flags"]
+	});
+
+	const guilds = await Member.find({
+		where: {
+			user: {
+				id: req.user_id,
+			},
+		},
+		relations: ["guild", "roles"],
+		//@ts-ignore
+		select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"]
+	});
+
+	const guildsWithPermissions = guilds.map(x => {
+		const perms = x.guild.owner_id === user.id
+			? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
+			: Permissions.finalPermission({
+				user: {
+					id: user.id,
+					roles: x.roles?.map(x => x.id) || [],
+				},
+				guild: {
+					roles: x?.roles || [],
+				}
+			});
+
+		return {
+			id: x.guild.id,
+			name: x.guild.name,
+			icon: x.guild.icon,
+			mfa_level: x.guild.mfa_level,
+			permissions: perms.bitfield.toString(),
+		};
+	});
+
+	return res.json({
+		guilds: guildsWithPermissions,
+		user: {
+			id: user.id,
+			username: user.username,
+			avatar: user.avatar,
+			avatar_decoration: null,	// TODO
+			discriminator: user.discriminator,
+			public_flags: user.public_flags,
+		},
+		application: {
+			id: app.id,
+			name: app.name,
+			icon: app.icon,
+			description: app.description,
+			summary: app.summary,
+			type: app.type,
+			hook: app.hook,
+			guild_id: null,	// TODO support guilds
+			bot_public: app.bot_public,
+			bot_require_code_grant: app.bot_require_code_grant,
+			verify_key: app.verify_key,
+			flags: app.flags,
+		},
+		bot: {
+			id: bot.id,
+			username: bot.username,
+			avatar: bot.avatar,
+			avatar_decoration: null,	// TODO
+			discriminator: bot.discriminator,
+			public_flags: bot.public_flags,
+			bot: true,
+			approximated_guild_count: 0,	// TODO
+		},
+		authorized: false,
+	});
+});
+
+router.post("/", route({ body: "ApplicationAuthorizeSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as ApplicationAuthorizeSchema;
+	const {
+		client_id,
+		scope,
+		response_type,
+		redirect_url
+	} = req.query;
+
+	// TODO: captcha verification
+	// TODO: MFA verification
+
+	const perms = await getPermission(req.user_id, body.guild_id, undefined, { member_relations: ["user"] });
+	// getPermission cache won't exist if we're owner
+	if (Object.keys(perms.cache || {}).length > 0 && perms.cache.member!.user.bot) throw DiscordApiErrors.UNAUTHORIZED;
+	perms.hasThrow("MANAGE_GUILD");
+
+	const app = await Application.findOne({
+		where: {
+			id: client_id as string,
+		},
+		relations: ["bot"],
+	});
+
+	// TODO: use DiscordApiErrors
+	// findOneOrFail throws code 404
+	if (!app) throw new ApiError("Unknown Application", 10002, 404);
+	if (!app.bot) throw new ApiError("OAuth2 application does not have a bot", 50010, 400);
+
+	await Member.addToGuild(app.id, body.guild_id);
+
+	return res.json({
+		location: "/oauth2/authorized",	// redirect URL
+	});
+});
+
+export default router;
diff --git a/src/util/schemas/ApplicationAuthorizeSchema.ts b/src/util/schemas/ApplicationAuthorizeSchema.ts
new file mode 100644
index 00000000..1a0aae0f
--- /dev/null
+++ b/src/util/schemas/ApplicationAuthorizeSchema.ts
@@ -0,0 +1,7 @@
+export interface ApplicationAuthorizeSchema {
+	authorize: boolean;
+	guild_id: string;
+	permissions: string;
+	captcha_key?: string;
+	code?: string;	// 2fa code
+}
\ No newline at end of file
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index a03cebe2..be4be0c7 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -58,4 +58,5 @@ export * from "./ChannelReorderSchema";
 export * from "./UserSettingsSchema";
 export * from "./BotModifySchema";
 export * from "./ApplicationModifySchema";
-export * from "./ApplicationCreateSchema";
\ No newline at end of file
+export * from "./ApplicationCreateSchema";
+export * from "./ApplicationAuthorizeSchema";
\ No newline at end of file