diff --git a/src/api/verification/verification.ts b/src/api/verification/verification.ts
new file mode 100644
index 0000000..6186991
--- /dev/null
+++ b/src/api/verification/verification.ts
@@ -0,0 +1,20 @@
+import { getAxiosClient } from "@/client/axios";
+
+type VerifyMemberPayload = {
+ guildId: string;
+ userId: string;
+ captchaToken: string;
+ token: string;
+};
+
+export const verifyMember = async (payload: VerifyMemberPayload) => {
+ const res = await getAxiosClient().post(
+ `/guilds/${encodeURIComponent(payload.guildId)}/members/${encodeURIComponent(payload.userId)}/verify`,
+ {
+ captchaToken: payload.captchaToken,
+ token: payload.token,
+ },
+ );
+
+ return res.data as { success: boolean; error?: string };
+};
diff --git a/src/app/(main)/not-found.tsx b/src/app/(main)/not-found.tsx
new file mode 100644
index 0000000..defd87f
--- /dev/null
+++ b/src/app/(main)/not-found.tsx
@@ -0,0 +1,14 @@
+import HTTPErrorView from "@/components/Errors/HTTPErrorView";
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "404 Not Found",
+};
+
+export default function NotFound() {
+ return (
+
+ The requested URL was not found on this server.
+
+ );
+}
diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx
index 333a58e..b7f0d64 100644
--- a/src/app/(main)/page.tsx
+++ b/src/app/(main)/page.tsx
@@ -1,6 +1,7 @@
import FeatureCard from "@/components/Cards/FeatureCard";
import HeadingDivider from "@/components/Dividers/HeadingDivider";
import Footer from "@/components/Layout/Footer";
+import { DOCS_URL, INVITE_REQUEST_URL } from "@/constants/links";
import activeDevelopmentImage from "@/images/active-development.png";
import autoModerationImage from "@/images/auto-moderation.png";
import backgroundImage from "@/images/background.svg";
@@ -76,21 +77,25 @@ export default function HomePage() {
}
size="lg"
color="primary"
variant="flat"
className="w-full md:w-auto"
+ href={INVITE_REQUEST_URL}
>
Invite SudoBot
}
size="lg"
color="primary"
variant="flat"
className="mt-3 w-full md:mt-0 md:w-auto"
+ href={`${DOCS_URL}/getting-started`}
>
Set up manually
@@ -99,21 +104,30 @@ export default function HomePage() {
-
Why SudoBot?
+
+ Why SudoBot?
+
-
+
-
+
Manual Moderation
- SudoBot provides a wide range of manual moderation tools to keep your server
+ SudoBot provides a wide range of manual
+ moderation tools to keep your server
safe and secure.
@@ -137,9 +151,12 @@ export default function HomePage() {
Smart Auto Moderation
- SudoBot includes a powerful auto-moderation system that understands your
- community’s needs and can automatically moderate your server, so you
- can focus on other things.
+ SudoBot includes a powerful
+ auto-moderation system that
+ understands your community’s
+ needs and can automatically moderate
+ your server, so you can focus on
+ other things.
@@ -150,9 +167,17 @@ export default function HomePage() {
"Message Scanning",
"Automatic Actions",
].map((feature) => (
-
-
- {feature}
+
+
+
+ {feature}
+
))}
@@ -169,14 +194,20 @@ export default function HomePage() {
-
+
Free & Open Source
- SudoBot is free and open source, respecting your freedom. It is licensed under{" "}
+ SudoBot is free and open source,
+ respecting your freedom. It is licensed
+ under{" "}
- GNU Affero General Public License v3.0
+ GNU Affero General Public License
+ v3.0
.
@@ -200,7 +231,8 @@ export default function HomePage() {
Active Development
- We are actively adding new features and fixing issues. We always welcome new
+ We are actively adding new features and
+ fixing issues. We always welcome new
feature requests or improvement ideas.
@@ -218,10 +250,14 @@ export default function HomePage() {
size="2rem"
className="mx-auto mb-3 block text-[rgb(0,100,255)]"
/>
- Self-Hosted
+
+ Self-Hosted
+
- Don’t want to host the bot yourself? We have a solution for that as well
- — you can invite our self-hosted instance!
+ Don’t want to host the bot
+ yourself? We have a solution for that as
+ well — you can invite our
+ self-hosted instance!
@@ -235,13 +271,17 @@ export default function HomePage() {
-
+
Highly Customizable
- SudoBot’s configuration system was built in a way so that you can
- customize almost everything the bot does.
+ SudoBot’s configuration system was
+ built in a way so that you can customize
+ almost everything the bot does.
@@ -264,8 +304,11 @@ export default function HomePage() {
Robust Permission System
- SudoBot uses Hybrid Permission System — you get to choose one of the three
- possible modes. By default, it relies on Discord’s permission system.
+ SudoBot uses Hybrid Permission
+ System — you get to choose one of
+ the three possible modes. By
+ default, it relies on Discord’s
+ permission system.
@@ -275,9 +318,17 @@ export default function HomePage() {
"Level-based Permission System",
"Overwrite-based Permission System",
].map((mode) => (
-
-
- {mode}
+
+
+
+ {mode}
+
))}
@@ -298,10 +349,14 @@ export default function HomePage() {
size="2rem"
className="mx-auto mb-3 block text-[rgb(0,100,255)]"
/>
-
Secure
+
+ Secure
+
- SudoBot is designed with security in mind. We take security seriously and are
- committed to protecting your data. Open Source always means more secure.
+ SudoBot is designed with security in
+ mind. We take security seriously and are
+ committed to protecting your data. Open
+ Source always means more secure.
diff --git a/src/app/(main)/verify/guilds/[id]/challenge/onboarding/page.tsx b/src/app/(main)/verify/guilds/[id]/challenge/onboarding/page.tsx
new file mode 100644
index 0000000..3157cb8
--- /dev/null
+++ b/src/app/(main)/verify/guilds/[id]/challenge/onboarding/page.tsx
@@ -0,0 +1,85 @@
+import PageExpired from "@/app/page-expired";
+import GuildVerificationGate from "@/components/GuildVerificationGate/GuildVerificationGate";
+import { Guild } from "@/types/Guild";
+import { ServerComponentProps } from "@/types/ServerComponentProps";
+import axios from "axios";
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { cache } from "react";
+
+const getVerificationInfo = cache(async (guildId: string, memberId: string) => {
+ try {
+ const info = await axios.get(
+ `${process.env.NEXT_PUBLIC_API_URL}/guilds/${encodeURIComponent(guildId)}/members/${encodeURIComponent(memberId)}/verify`,
+ );
+ return [info.data?.guild as Guild, null] as const;
+ } catch (error) {
+ return [null, error] as const;
+ }
+});
+
+export async function generateMetadata({
+ params,
+ searchParams,
+}: ServerComponentProps): Promise
{
+ const id = params?.id;
+ const userId = searchParams?.u;
+ const requestToken = searchParams?.t;
+
+ if (!id || !userId || !requestToken) {
+ return {
+ title: "419 Page Expired - SudoBot",
+ };
+ }
+
+ const [guild, error] = id
+ ? await getVerificationInfo(id, userId)
+ : [null, true];
+
+ if (!guild || error) {
+ return {
+ title: "404 Not Found - SudoBot",
+ };
+ }
+
+ return {
+ title: "Verify to Continue - SudoBot",
+ robots: {
+ index: false,
+ follow: false,
+ },
+ };
+}
+
+export default async function VerifyOnboardingPage({
+ params,
+ searchParams,
+}: ServerComponentProps) {
+ const id = params?.id;
+ const userId = searchParams?.u;
+ const requestToken = searchParams?.t;
+
+ if (!id || !userId || !requestToken) {
+ return ;
+ }
+
+ const [guild, error] = id
+ ? await getVerificationInfo(id, userId)
+ : [null, true];
+
+ console.log(guild, error);
+
+ if (!guild || error) {
+ notFound();
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/page-expired.tsx b/src/app/page-expired.tsx
new file mode 100644
index 0000000..6eb9cac
--- /dev/null
+++ b/src/app/page-expired.tsx
@@ -0,0 +1,15 @@
+import HTTPErrorView from "@/components/Errors/HTTPErrorView";
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "419 Page Expired",
+};
+
+export default function PageExpired() {
+ return (
+
+ The page has either expired due to inactivity or the request payload
+ was invalid.
+
+ );
+}
diff --git a/src/components/GuildVerificationGate/GuildVerificationGate.tsx b/src/components/GuildVerificationGate/GuildVerificationGate.tsx
new file mode 100644
index 0000000..99a0cae
--- /dev/null
+++ b/src/components/GuildVerificationGate/GuildVerificationGate.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { verifyMember } from "@/api/verification/verification";
+import { logger } from "@/logging/logger";
+import { Guild } from "@/types/Guild";
+import { LinearProgress } from "@mui/material";
+import { Spacer } from "@nextui-org/react";
+import { useMutation } from "@tanstack/react-query";
+import { AxiosError } from "axios";
+import { FC, ReactNode, useEffect, useRef, useState } from "react";
+import { BsCheckCircle } from "react-icons/bs";
+
+type GuildVerificationGateProps = {
+ guild: Guild;
+ userId: string;
+ requestToken: string;
+};
+
+const SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
+
+declare var window: Window & {
+ turnstile: {
+ ready: (callback: () => void) => void;
+ render: (selector: string, options: any) => void;
+ remove: (selector: string) => void;
+ reset: (selector?: string) => void;
+ };
+ turnstileOnLoad?: () => void;
+};
+
+const GuildVerificationGate: FC = ({
+ guild,
+ userId,
+ requestToken,
+}) => {
+ const [turnstileScript, setTurnstileScript] = useState(null);
+ const hasLoadedRef = useRef(false);
+ const { mutate, error, isPending, isSuccess, isError } = useMutation({
+ mutationFn: verifyMember,
+ onError: () => window.turnstile.reset("#turnstile-container"),
+ });
+
+ useEffect(() => {
+ setTurnstileScript(() => {
+ return (
+
+ );
+ });
+
+ if (!hasLoadedRef.current) {
+ window.turnstileOnLoad ??= () => {
+ logger.debug(GuildVerificationGate.name, "Turnstile Ready");
+
+ window.turnstile.render("#turnstile-container", {
+ sitekey: SITE_KEY,
+ callback: (token: string) => {
+ mutate({
+ guildId: guild.id,
+ userId,
+ captchaToken: token,
+ token: requestToken,
+ });
+ },
+ });
+ };
+
+ hasLoadedRef.current = true;
+ }
+
+ if (window.turnstile) {
+ window.turnstileOnLoad?.();
+ }
+
+ return () => {
+ window.turnstileOnLoad = undefined;
+
+ try {
+ window.turnstile.reset("#turnstile-container");
+ } catch {}
+ };
+ }, []);
+
+ return (
+
+ {turnstileScript}
+
Verify
+
+ to continue to{" "}
+
+ {guild.name}
+
+
+
+
+ {!isSuccess && (
+
+ This server requires verification for new members. To
+ verify yourself, complete the challenge below.
+
+ )}
+ {isPending && (
+
+
+
+ )}
+ {!isSuccess &&
}
+ {isSuccess && (
+
+
+
+ Verification successful!
+
+
+ You can now close this tab or window.
+
+
+ )}
+ {isError && (
+
+
+ {(error instanceof AxiosError
+ ? error?.response?.data?.error
+ : null) ?? "We were unable to verify you."}
+
+
+ )}
+
+
+ Enable JavaScript to continue!
+
+
+
+
+ );
+};
+
+export default GuildVerificationGate;
diff --git a/src/components/Navigation/NavbarMobile.tsx b/src/components/Navigation/NavbarMobile.tsx
index ee064bb..4862815 100644
--- a/src/components/Navigation/NavbarMobile.tsx
+++ b/src/components/Navigation/NavbarMobile.tsx
@@ -43,7 +43,11 @@ export default function NavbarMobile({ open, setOpen }: NavbarMobileProps) {
text: "text-base",
}}
/>
- setOpen(false)} sx={{ minWidth: 0 }} className="text-black dark:text-white">
+ setOpen(false)}
+ sx={{ minWidth: 0 }}
+ className="text-black dark:text-white"
+ >
@@ -53,7 +57,12 @@ export default function NavbarMobile({ open, setOpen }: NavbarMobileProps) {
-
+
@@ -78,7 +87,12 @@ export default function NavbarMobile({ open, setOpen }: NavbarMobileProps) {
}
}}
>
-
+
{item.title}
@@ -115,6 +129,30 @@ export default function NavbarMobile({ open, setOpen }: NavbarMobileProps) {
))}
+
+ {!isLoggedIn && (
+
+ {
+ if (pathname !== "/login") {
+ setOpen(false);
+ }
+ }}
+ >
+ Login
+
+
+ )}
>
diff --git a/src/config/navbar.ts b/src/config/navbar.ts
index ce2d580..e9419f8 100644
--- a/src/config/navbar.ts
+++ b/src/config/navbar.ts
@@ -1,7 +1,7 @@
import {
DOCS_URL,
INVITE_REQUEST_URL,
- SUPPORT_EMAIL_ADDRESS
+ SUPPORT_EMAIL_ADDRESS,
} from "@/constants/links";
export const links = [
@@ -20,10 +20,6 @@ export const links = [
{
title: "Invite",
href: INVITE_REQUEST_URL,
- },
- {
- title: "Login",
- href: "/login",
- mobileOnly: true,
+ mobileOnly: false,
},
];
diff --git a/src/types/ServerComponentProps.ts b/src/types/ServerComponentProps.ts
new file mode 100644
index 0000000..6960b79
--- /dev/null
+++ b/src/types/ServerComponentProps.ts
@@ -0,0 +1,4 @@
+export type ServerComponentProps = {
+ searchParams?: Record;
+ params?: Record;
+};