Browse Source

Start work on verify

ghorsington 3 years ago
parent
commit
de929c7ca2

+ 10 - 3
bot/src/rpc.ts

@@ -15,11 +15,12 @@ const PORT = +(process.env.RPC_PORT ?? "8181");
 
 const app = express();
 
-async function checkUser(userId: string, check: (user: GuildMember) => Promise<boolean>): Promise<boolean> {
+async function checkUser(userId: string, check: (user: GuildMember, verification: GuildVerification) => Promise<boolean>): Promise<boolean> {
     const verificationGuildRepo = getRepository(GuildVerification);
     const guilds = await verificationGuildRepo.find({
         select: [
-            "guildId"
+            "guildId",
+            "verifiedRoleId",
         ]
     });
     for (const guild of guilds) {
@@ -29,7 +30,7 @@ async function checkUser(userId: string, check: (user: GuildMember) => Promise<b
             continue;
         }
         const user = await tryDo(guildInstance.result.members.fetch(userId));
-        if (user.ok && await check(user.result)) {
+        if (user.ok && await check(user.result, guild)) {
             return true;
         }
     }
@@ -48,6 +49,12 @@ const handler: ModuleRpcServer.ServiceHandlerFor<typeof NoctBotService> = {
     async userAuthorised({ userId }): Promise<{ authorised: boolean }> {
         return { authorised: await checkUser(userId, (user) => isAuthorisedAsync(user)) };
     },
+    async userVerified({ userId }): Promise<{ verified: boolean }> {
+        return { verified: await checkUser(userId, async (user, guild) => user.roles.cache.has(guild.verifiedRoleId)) };
+    },
+    async verifyUser({ userId }): Promise<{ ok: boolean }> {
+        return { ok: !(await checkUser(userId, async (user, guild) => !(await tryDo(user.roles.add(guild.verifiedRoleId))).ok)) };
+    }
 };
 
 app.use(ModuleRpcProtocolServer.registerRpcRoutes(NoctBotService, handler));

+ 3 - 0
shared/src/db/entity/GuildVerification.ts

@@ -4,4 +4,7 @@ import {Entity, PrimaryColumn, Column} from "typeorm";
 export class GuildVerification {
     @PrimaryColumn()
     guildId: string;
+
+    @Column()
+    verifiedRoleId: string;
 }

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

@@ -10,5 +10,13 @@ export const NoctBotService = {
     userAuthorised: {
         request: {} as { userId: string },
         response: {} as { authorised: boolean },
+    },
+    userVerified: {
+        request: {} as { userId: string },
+        response: {} as { verified: boolean },
+    },
+    verifyUser: {
+        request: {} as { userId: string },
+        response: {} as { ok: boolean },
     }
 };

+ 45 - 12
web/src/routes/rules/index.svelte

@@ -17,6 +17,7 @@
     return {
       rulesText: md.ok ? md.text : "",
       sitekey: verify.captchaSitekey,
+      verified: verify.userVerified,
     };
   }
 </script>
@@ -28,12 +29,20 @@
   import { faSpinner } from "@fortawesome/free-solid-svg-icons";
   import { fade } from "svelte/transition";
 
-  export let rulesText!: string;
-  export let sitekey!: string;
+  export let rulesText: string = "";
+  export let sitekey: string = "";
+  export let verified!: boolean;
+  console.log(verified);
+  let showVerify: boolean = false;
   const spinner = (faSpinner as unknown) as undefined;
   let htmlContent = new Converter().makeHtml(rulesText);
 
-  enum State { None, Verify, Error, Message };
+  enum State {
+    None,
+    Verify,
+    Error,
+    Message,
+  }
 
   let state: State = State.None;
   let message: string = "";
@@ -42,6 +51,16 @@
     state = State.Verify;
     // message = "wew";
   }
+
+  function onExpired() {
+    state = State.Error;
+    message = "Captcha expired, please solve it again";
+  }
+
+  function onError(error: string) {
+    state = State.Error;
+    message = `${error}; Please try refreshing the page.`;
+  }
 </script>
 
 <style>
@@ -71,15 +90,29 @@
   <div>
     {@html htmlContent}
   </div>
-  <div class="flex flex-col flex-wrap items-center">
-    <a href="asd" on:click|preventDefault={() => onVerified({ key: "wew" })}>Verify test</a>
-    <!-- <h-captcha site-key={sitekey} dark on:verified={onVerified} /> -->
-    <div class="py-2">
-      {#if state == State.Verify}
-        <span transition:fade={{ duration: 50 }}><Icon data={spinner} spin /> Verifying</span>
-        {:else if state == State.Error || state == State.Message }
-        <span class:message={state == State.Message} class:error={state == State.Error}>{message}</span>
+  {#if !verified}
+    <div class="flex flex-col flex-wrap items-center">
+      <form class="py-4">
+        <input type="checkbox" id="show-verify" bind:checked={showVerify} disabled={state != State.None} />
+        <label for="show-verify">I understand these rules and agree to abide to
+          them</label>
+      </form>
+      {#if showVerify}
+        <a
+          href="asd"
+          on:click|preventDefault={() => onVerified({ key: 'wew' })}>Verify test</a>
+        <!-- <h-captcha site-key={sitekey} dark on:verified={onVerified} on:expired={onExpired} on:error={onError} />   -->
       {/if}
+      <div class="py-2 pointer-events-none select-none">
+        {#if state == State.Verify}
+          <span transition:fade={{ duration: 50 }}><Icon data={spinner} spin />
+            Verifying</span>
+        {:else if state == State.Error || state == State.Message}
+          <span
+            class:message={state == State.Message}
+            class:error={state == State.Error}>{message}</span>
+        {/if}
+      </div>
     </div>
-  </div>
+  {/if}
 </div>

+ 2 - 1
web/src/routes/rules/md.ts

@@ -29,7 +29,8 @@ export const get = async (req: ExpressRequest, res: ExpressResponse): GetResult
 type PostResult = Promise<ExpressResponse<Option<unknown, { error: string }>>>;
 export const post = async (req: ExpressRequest, res: ExpressResponse): PostResult => {
     const isText = (body: unknown):
-        body is MDText => (body as Record<string, unknown>).text !== undefined;
+        body is MDText => body instanceof Object
+                            && (body as Record<string, unknown>).text !== undefined;
 
     if (!isText(req.body)) {
         return res.json({

+ 85 - 7
web/src/routes/rules/verify.ts

@@ -2,19 +2,97 @@ import { Request as ExpressRequest, Response as ExpressResponse } from "express"
 import { existsSync, promises } from "fs";
 import { join } from "path";
 import { ENVIRONMENT } from "src/utils/environment";
-import { Option } from "@shared/common/async_utils";
+import { Option, tryDo } from "@shared/common/async_utils";
 import { rpcClient } from "src/utils/rpc";
+import got from "got";
+import { logger } from "src/utils/logging";
 
 export interface VerifyInfo {
     captchaSitekey: string;
+    userVerified: boolean;
 }
 
+export interface VerifyRequest {
+    captchaResponse: string;
+}
+
+interface HCaptchaResponse {
+    success: boolean;
+    "error-codes"?: string[];
+}
+
+const VERIFY_URL = "https://hcaptcha.com/siteverify";
+
 type GetResult = Promise<ExpressResponse<VerifyInfo>>;
-export const get = async (req: ExpressRequest, res: ExpressResponse): GetResult => res.json({
-    captchaSitekey: ENVIRONMENT.hCaptchaSitekey,
-});
+export const get = async (req: ExpressRequest, res: ExpressResponse): GetResult => {
+    if (!req.session?.userId) {
+        return res.json({
+            captchaSitekey: ENVIRONMENT.hCaptchaSitekey,
+            userVerified: true,
+        });
+    }
+    return res.json({
+        captchaSitekey: ENVIRONMENT.hCaptchaSitekey,
+        userVerified: (await rpcClient.userVerified({ userId: req.session.userId })).verified,
+    });
+};
+
+type PostResult = Promise<ExpressResponse<Option<unknown, { error: string }>>>;
+export const post = async (req: ExpressRequest, res: ExpressResponse): PostResult => {
+    const hasToken = (body: unknown):
+        body is VerifyRequest => body instanceof Object
+                                    && (body as VerifyRequest).captchaResponse !== undefined;
+    if (!hasToken(req.body)) {
+        return res.json({
+            ok: false,
+            error: "No user token provided, please try again",
+        });
+    }
+
+    if (!req.session?.userId) {
+        return res.json({
+            ok: false,
+            error: "Not logged in, please log in",
+        });
+    }
+
+    const response = await tryDo(got<HCaptchaResponse>(VERIFY_URL, {
+        method: "post",
+        responseType: "json",
+        form: {
+            secret: ENVIRONMENT.hCaptchaSecret,
+            response: req.body.captchaResponse,
+        },
+    }));
+
+    if (!response.ok) {
+        logger.error("Failed to hCaptcha user %s: %s", req.session.userId, response.error);
+        return res.json({
+            ok: false,
+            error: "Failed to verify hCaptcha response. Please try again.",
+        });
+    }
+
+    const captchaResponse = response.result.body;
 
-// type PostResult = Promise<ExpressResponse<Option<unknown, { error: string }>>>;
-// export const post = async (req: ExpressRequest, res: ExpressResponse): PostResult => {
+    if (!captchaResponse.success) {
+        const errors = captchaResponse["error-codes"] ?? [];
+        logger.error("Failed hCaptcha verify on user %s. Got errors: %s", req.session.userId, errors.join(";"));
+        return res.json({
+            ok: false,
+            error: "Failed to verify hCaptcha response. Please try again.",
+        });
+    }
 
-// };
+    const verifyResponse = await rpcClient.verifyUser({ userId: req.session.userId });
+    if (!verifyResponse.ok) {
+        logger.error("Failed to verify user %s (%s)", req.session.userId, req.session.username);
+        return res.json({
+            ok: false,
+            error: "Failed to verify your account. Please try again or contact server owners.",
+        });
+    }
+    return res.json({
+        ok: true,
+    });
+};