summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/middlewares/Authentication.ts2
-rw-r--r--src/api/routes/oauth2/callback.ts38
-rw-r--r--src/api/routes/policies/instance/stats.ts21
-rw-r--r--src/api/util/handlers/Oauth.ts83
-rw-r--r--src/api/util/index.ts3
-rw-r--r--src/util/entities/Config.ts12
-rw-r--r--src/util/interfaces/Event.ts49
7 files changed, 185 insertions, 23 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index ecde7fb4..ec2c42e5 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -25,6 +25,8 @@ export const NO_AUTHORIZATION_ROUTES = [
 	"/track",
 	// Public policy pages
 	"/policies/instance",
+	// Oauth callback
+	"/oauth2/callback",
 	// Asset delivery
 	/\/guilds\/\d+\/widget\.(json|png)/,
 ];
diff --git a/src/api/routes/oauth2/callback.ts b/src/api/routes/oauth2/callback.ts
new file mode 100644
index 00000000..3c7fb777
--- /dev/null
+++ b/src/api/routes/oauth2/callback.ts
@@ -0,0 +1,38 @@
+import { Router, Request, Response } from "express";
+import { route, OauthCallbackHandlers } from "@fosscord/api";
+import { FieldErrors, generateToken, User } from "@fosscord/util";
+const router = Router();
+
+router.get("/:type", route({}), async (req: Request, res: Response) => {
+	const { type } = req.params;
+	const handler = OauthCallbackHandlers[type];
+	if (!handler) throw FieldErrors({
+		type: {
+			code: "BASE_TYPE_CHOICES",
+			message: `Value must be one of (${Object.keys(OauthCallbackHandlers).join(", ")}).`,
+		}
+	});
+
+	const { code } = req.query;
+	if (!code || typeof code !== "string") throw FieldErrors({ code: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED"), } });
+	const access = await handler.getAccessToken(code);
+
+	const oauthUser = await handler.getUserDetals(access.access_token);
+
+	let user = await User.findOne({ where: { email: oauthUser.email } });
+	if (!user) {
+		user = await User.register({
+			email: oauthUser.email,
+			username: oauthUser.username,
+			req
+		});
+
+		// TODO: upload pfp, banner?
+	}
+
+	const token = await generateToken(user.id);
+
+	return { token };
+});
+
+export default router;
\ No newline at end of file
diff --git a/src/api/routes/policies/instance/stats.ts b/src/api/routes/policies/instance/stats.ts
new file mode 100644
index 00000000..fb8c386a
--- /dev/null
+++ b/src/api/routes/policies/instance/stats.ts
@@ -0,0 +1,21 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { Attachment, Config, Guild, Message, RateLimit, Session, User } from "@fosscord/util";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json({
+		all_time: {
+			users: await User.count(),
+			guilds: await Guild.count(),
+			messages: await Message.count(),
+			attachments: await Attachment.count(),
+		},
+		now: {
+			sessions: await Session.count(),
+			rate_limits: await RateLimit.count(),
+		}
+	});
+});
+
+export default router;
diff --git a/src/api/util/handlers/Oauth.ts b/src/api/util/handlers/Oauth.ts
new file mode 100644
index 00000000..cc662161
--- /dev/null
+++ b/src/api/util/handlers/Oauth.ts
@@ -0,0 +1,83 @@
+// TODO: Puyo's connections PR would replace this file
+
+import { Config } from "@fosscord/util";
+import fetch from "node-fetch";
+
+export interface OauthAccessToken {
+	access_token: string;
+	token_type: string;
+	expires_in: string;
+	refresh_token: string;
+	scope: string;
+};
+
+export interface OauthUserDetails {
+	id: string;
+	email: string;
+	username: string;
+	avatar_url: string | null;
+}
+
+interface Connection {
+	getAccessToken: (code: string) => Promise<OauthAccessToken>;
+	getUserDetals: (token: string) => Promise<OauthUserDetails>;
+}
+
+const DiscordConnection: Connection = {
+	getAccessToken: async (code) => {
+		const { external } = Config.get();
+		const { discord } = external;
+
+		if (!discord.id || !discord.secret || !discord.redirect)
+			throw new Error("Discord Oauth has not been configured.")
+
+		const body = new URLSearchParams(
+			Object.entries({
+				client_id: discord.id as string,
+				client_secret: discord.secret as string,
+				redirect_uri: discord.redirect as string,
+				code: code as string,
+				grant_type: "authorization_code",
+			})
+		).toString();
+
+		const resp = await fetch("https://discord.com/api/oauth2/token", {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/x-www-form-urlencoded",
+			},
+			body: body,
+		});
+		if (resp.status !== 200) throw new Error(`Failed to get access token.`,);
+
+		const json = await resp.json();
+
+		return json;
+	},
+
+	getUserDetals: async (token) => {
+		const resp = await fetch("https://discord.com/api/users/@me", {
+			headers: {
+				Authorization: `Bearer ${token}`
+			},
+		});
+
+		const json = await resp.json();
+		if (!json.username || !json.email) throw new Error("Failed to get user details via oauth");
+
+		return {
+			id: json.id,
+			email: json.email,
+			username: json.username,
+			avatar_url: json.avatar
+				? `https://cdn.discordapp.com/avatars/${json.id}/${json.avatar}?size=2048`
+				: null,
+		};
+	}
+};
+
+const OauthCallbackHandlers: { [key: string]: Connection; } = {
+	discord: DiscordConnection
+};
+
+export { OauthCallbackHandlers };
\ No newline at end of file
diff --git a/src/api/util/index.ts b/src/api/util/index.ts
index ffad0607..49542ceb 100644
--- a/src/api/util/index.ts
+++ b/src/api/util/index.ts
@@ -7,4 +7,5 @@ export * from "./handlers/route";
 export * from "./utility/String";
 export * from "./handlers/Voice";
 export * from "./utility/captcha";
-export * from "./utility/EmbedHandlers";
\ No newline at end of file
+export * from "./utility/EmbedHandlers";
+export * from "./handlers/Oauth";
\ No newline at end of file
diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts
index 5035f552..9b25795d 100644
--- a/src/util/entities/Config.ts
+++ b/src/util/entities/Config.ts
@@ -211,7 +211,12 @@ export interface ConfigValue {
 	};
 	external: {
 		twitter: string | null;
-	}
+		discord: {
+			id: string | null;
+			secret: string | null;
+			redirect: string | null;
+		};
+	};
 }
 
 export const DefaultConfigOptions: ConfigValue = {
@@ -423,5 +428,10 @@ export const DefaultConfigOptions: ConfigValue = {
 	},
 	external: {
 		twitter: null,
+		discord: {
+			id: null,
+			secret: null,
+			redirect: null,
+		}
 	}
 };
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index 8048250c..5e474d9e 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -1,19 +1,26 @@
-import { PublicUser, User, UserSettings } from "../entities/User";
-import { Channel } from "../entities/Channel";
-import { Guild } from "../entities/Guild";
-import { Member, PublicMember, UserGuildSettings } from "../entities/Member";
-import { Emoji } from "../entities/Emoji";
-import { Role } from "../entities/Role";
-import { Invite } from "../entities/Invite";
-import { Message, PartialEmoji } from "../entities/Message";
-import { VoiceState } from "../entities/VoiceState";
-import { ApplicationCommand } from "../entities/Application";
-import { Interaction } from "./Interaction";
-import { ConnectedAccount } from "../entities/ConnectedAccount";
-import { Relationship, RelationshipType } from "../entities/Relationship";
-import { Presence } from "./Presence";
-import { Sticker } from "..";
-import { Activity, Status } from ".";
+import {
+	RelationshipType,
+	ConnectedAccount,
+	Interaction,
+	ApplicationCommand,
+	VoiceState,
+	Message,
+	PartialEmoji,
+	Invite,
+	Role,
+	Emoji,
+	PublicMember,
+	UserGuildSettings,
+	Guild,
+	Channel,
+	PublicUser,
+	User,
+	Sticker,
+	Activity,
+	Status,
+	Presence,
+	UserSettings,
+} from "@fosscord/util";
 
 export interface Event {
 	guild_id?: string;
@@ -73,9 +80,9 @@ export interface ReadyEventData {
 		number,
 		null,
 		number,
-		[[number, { e: number; s: number }[]]],
+		[[number, { e: number; s: number; }[]]],
 		[number, [[number, [number, number]]]],
-		{ b: number; k: bigint[] }[],
+		{ b: number; k: bigint[]; }[],
 	][];
 	guild_join_requests?: any[]; // ? what is this? this is new
 	shard?: [number, number];
@@ -473,7 +480,7 @@ export interface SessionsReplace extends Event {
 export interface GuildMemberListUpdate extends Event {
 	event: "GUILD_MEMBER_LIST_UPDATE";
 	data: {
-		groups: { id: string; count: number }[];
+		groups: { id: string; count: number; }[];
 		guild_id: string;
 		id: string;
 		member_count: number;
@@ -481,8 +488,8 @@ export interface GuildMemberListUpdate extends Event {
 		ops: {
 			index: number;
 			item: {
-				member?: PublicMember & { presence: Presence };
-				group?: { id: string; count: number }[];
+				member?: PublicMember & { presence: Presence; };
+				group?: { id: string; count: number; }[];
 			};
 		}[];
 	};