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() {
@@ -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."} +

    +
    + )} + +
    +
    + ); +}; + +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", }} /> -
    @@ -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; +};