Browse Source

web: login verification

ghorsington 3 years ago
parent
commit
711e4e3b53

+ 16 - 1
bot/src/rpc.ts

@@ -4,15 +4,30 @@ import { ModuleRpcServer } from "rpc_ts/lib/server";
 import { ModuleRpcProtocolServer } from "rpc_ts/lib/protocol/server";
 import { NoctBotService } from "@shared/rpc/backend";
 import { logger } from "./logging";
+import { client } from "./client";
+import { tryDo } from "@shared/common/async_utils";
 
 const PORT = +(process.env.RPC_PORT ?? "8181");
 
-
 const app = express();
 
 const handler: ModuleRpcServer.ServiceHandlerFor<typeof NoctBotService> = {
     async getPing({ ping }): Promise<{ text: string }> {
         return { text: `pong: ${ping}` };
+    },
+
+    async userInServer({ userId }): Promise<{ exists: boolean }> {
+        const res = await tryDo(client.bot.users.fetch(userId));
+        if (!res.ok) {
+            return { exists: false };
+        }
+        for (const [_, g] of client.bot.guilds.cache.entries()) {
+            const res = await tryDo(g.members.fetch(userId));
+            if (res.ok) {
+                return { exists: true };
+            }
+        }
+        return { exists: false };
     }
 };
 

+ 4 - 0
shared/src/rpc/backend.ts

@@ -2,5 +2,9 @@ export const NoctBotService = {
     getPing: {
         request: {} as { ping: string },
         response: {} as { text: string },
+    },
+    userInServer: {
+        request: {} as { userId: string },
+        response: {} as { exists: boolean },
     }
 };

+ 1 - 1
web/rollup.config.js

@@ -113,7 +113,7 @@ export default {
         ],
         external: Object.keys(pkg.dependencies).concat(
             require("module").builtinModules || Object.keys(process.binding("natives")), // eslint-disable-line global-require
-        ).concat("winston"),
+        ).concat(/rpc_ts/),
 
         preserveEntrySignatures: "strict",
         onwarn,

+ 2 - 0
web/src/cookie.d.ts

@@ -1,5 +1,7 @@
 declare namespace CookieSessionInterfaces {
     interface CookieSessionObject {
         authTokenCode?: string;
+        userId?: string;
+        username?: string;
     }
 }

+ 23 - 7
web/src/routes/login/discord.ts

@@ -1,11 +1,12 @@
 import { Request as ExpressRequest, Response as ExpressResponse } from "express";
-import { OAuth2 } from "src/utils/util";
+import { DiscordAPI } from "src/utils/util";
 import { ENVIRONMENT } from "src/utils/environment";
-import { Option } from "@shared/common/async_utils";
+import { Option, tryDo } from "@shared/common/async_utils";
 import { logger } from "src/utils/logging";
+import { rpcClient } from "src/utils/rpc";
 
 export const get = async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
-    res.redirect(OAuth2.getAuthUrl({
+    res.redirect(DiscordAPI.getAuthUrl({
         client_id: ENVIRONMENT.clientId,
         redirect_url: ENVIRONMENT.redirectUrl,
         response_type: "code",
@@ -31,7 +32,7 @@ export const post = async (req: ExpressRequest, res: ExpressResponse):
             error: "Authentication token is missing. Please try logging in again.",
         });
     }
-    const result = await OAuth2.getToken({
+    const tokenResult = await DiscordAPI.getToken({
         client_id: ENVIRONMENT.clientId,
         client_secret: ENVIRONMENT.clientSecret,
         grant_type: "authorization_code",
@@ -39,10 +40,25 @@ export const post = async (req: ExpressRequest, res: ExpressResponse):
         scope: "identify",
         redirect_uri: ENVIRONMENT.redirectUrl,
     });
-    if (!result.ok) {
-        return res.json(result);
+    if (!tokenResult.ok) {
+        return res.json(tokenResult);
     }
-    req.sessionOptions.maxAge = result.expires_in;
+    const userResult = await DiscordAPI.getCurrentUser(tokenResult.access_token);
+    if (!userResult.ok) {
+        return res.json(userResult);
+    }
+    const userInServerResult = await tryDo(rpcClient.userInServer({ userId: userResult.id }));
+    if (!userInServerResult.ok) {
+        logger.error("WEB: failed to auth user %s: %s", userResult.id, userInServerResult.error);
+        return res.json({ ok: false, error: "Couldn't determine if user joined the server, please try again later" });
+    }
+    if (!userInServerResult.result.exists) {
+        return res.json({ ok: false, error: "You haven't joined any servers NoctBot manages! Please join first and try again!" });
+    }
+    req.session.authTokenCode = undefined;
+    req.session.userId = userResult.id;
+    req.session.username = `${userResult.username}#${userResult.discriminator}`;
+    req.sessionOptions.maxAge = tokenResult.expires_in;
     return res.json({
         ok: true,
     });

+ 7 - 4
web/src/routes/login/discord_auth.svelte

@@ -1,12 +1,13 @@
 <script lang="typescript">
 	import Icon from "svelte-awesome/components/Icon.svelte";
 	import { faSpinner } from "@fortawesome/free-solid-svg-icons";
-    import { onMount } from "svelte";
+    import { onMount, tick } from "svelte";
     import { Option } from "@shared/common/async_utils";
     import { goto } from "@sapper/app";
 
     const spinner = faSpinner as unknown as undefined;
     let error = "";
+    let ok = true;
     
 
     onMount(
@@ -16,7 +17,8 @@
             });
             const result = await res.json() as Option<unknown, {error: string}>;
             if (!result.ok) {
-                result.error = error;
+                ok = result.ok;
+                error = result.error;
             } else {
                 await goto("/");
             }
@@ -27,10 +29,11 @@
 <div class="flex items-center justify-center bg-gray-900 h-screen w-screen">
 	<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
         <h1 class="text-white text-4xl font-light">Logging in</h1>
-        {#if !error}
+        {#if ok}
             <div class="text-white"><Icon data={spinner} spin scale={1.5} /></div>
         {:else}
-            <div>Oh nooo!</div>
+            <div class="text-red-600 max-w-md py-4">{error}</div>
+            <a class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white hover:text-gray-100 rounded-sm" href="/">Back to login</a>
         {/if}
 	</div>
 </div>

+ 41 - 5
web/src/utils/util.ts

@@ -2,8 +2,9 @@
 import * as querystring from "querystring";
 import { Option, tryDo, isHttpError } from "@shared/common/async_utils";
 import got, { HTTPError } from "got";
+import { logger } from "./logging";
 
-const OAUTH_API = "https://discord.com/api/oauth2";
+const DISCORD_API = "https://discord.com/api";
 
 export interface AccessTokenResponse {
     access_token: string;
@@ -32,22 +33,57 @@ export type AuthorizeRequest = {
     scope: string,
 }
 
-export class OAuth2 {
+export interface DiscordUser {
+    id: string;
+    username: string;
+    discriminator: string;
+    avatar?: string;
+    bot?: boolean;
+    mfa_enabled?: boolean;
+    locale?: string;
+    verified?: boolean;
+    email?: string;
+    flags?: number;
+    premium_type?: number;
+}
+
+export class DiscordAPI {
     static getAuthUrl(opts: AuthorizeRequest): string {
-        return `${OAUTH_API}/authorize?${querystring.stringify(opts)}`;
+        return `${DISCORD_API}/oauth2/authorize?${querystring.stringify(opts)}`;
     }
 
     static async getToken(opts: AccessTokenRequest):
                         Promise<Option<AccessTokenResponse, { error: string }>> {
-        const result = await tryDo(got<AccessTokenResponse>(`${OAUTH_API}/token`, {
+        const result = await tryDo(got<AccessTokenResponse>("oauth2/token", {
             responseType: "json",
             method: "post",
             form: opts,
+            prefixUrl: DISCORD_API,
+        }));
+
+        if (!result.ok) {
+            if (isHttpError<HTTPError>(result.error)) {
+                logger.warn("WEB: Failed to authenticate a user: %s", result.error);
+                return { error: "Couldn't authenticate a user (the session might be old). Please try logging in again.", ok: false };
+            }
+            logger.error("WEB: failed to fetch access token: %s", result.error);
+            return { error: "Unexpected error", ok: false };
+        }
+        return { ok: true, ...result.result.body };
+    }
+
+    static async getCurrentUser(token: string): Promise<Option<DiscordUser, {error: string}>> {
+        const result = await tryDo(got<DiscordUser>("users/@me", {
+            responseType: "json",
+            headers: {
+                Authorization: `Bearer ${token}`,
+            },
+            prefixUrl: DISCORD_API,
         }));
 
         if (!result.ok) {
             if (isHttpError<HTTPError>(result.error)) {
-                return { error: `Failed to authenticate. Error: ${result.error.message}`, ok: false };
+                return { error: `Failed to check used ID. Error: ${result.error.message}`, ok: false };
             }
             return { error: "Unexpected error", ok: false };
         }