From e991e00f325d003d68e8ac710c4ee8dfb4bdca4c Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:25:58 +1100 Subject: Move src-slowcord to own repo https://github.com/MaddyUnderStars/slowcord-services --- src/api/middlewares/Authentication.ts | 2 + src/api/routes/oauth2/callback.ts | 38 ++++++++++++++ src/api/routes/policies/instance/stats.ts | 21 ++++++++ src/api/util/handlers/Oauth.ts | 83 +++++++++++++++++++++++++++++++ src/api/util/index.ts | 3 +- src/util/entities/Config.ts | 12 ++++- src/util/interfaces/Event.ts | 49 ++++++++++-------- 7 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 src/api/routes/oauth2/callback.ts create mode 100644 src/api/routes/policies/instance/stats.ts create mode 100644 src/api/util/handlers/Oauth.ts (limited to 'src') 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; + getUserDetals: (token: string) => Promise; +} + +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; }[]; }; }[]; }; -- cgit 1.4.1