"use client"; // Discord's OAuth redirect target for "Connect a Discord server" // (server-26#225 S7). Registered as each radio bot app's redirect URI: // https://drb.cusano.net/discord/connected // // middleware.ts lets this path through without the drb_session cookie, for // two reasons: a signed-out visitor would otherwise be redirected to /login // with the query (code + state) dropped; and drb_session is SameSite=Strict, // so even a signed-in user's cookie is not sent on the cross-site redirect // back from discord.com. This page does its own auth check instead. import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/components/AuthProvider"; import { Card } from "@/components/ui/Card"; import { LinkButton } from "@/components/ui/Button"; import { parseC2Error } from "@/lib/discord"; import { OAUTH_CALLBACK_PATH, type OAuthCallback, type OAuthCompleteResult, captureCallback, clearCallback, completeDiscordOAuth, } from "@/lib/discordOAuth"; type View = | { kind: "working" } | { kind: "none" } | { kind: "denied" } | { kind: "error"; message: string } | { kind: "done"; result: OAuthCompleteResult }; const SETTINGS = "/settings/organization"; function nextStepLabel(path: string): string { if (path.startsWith("/nodes/")) return "Add a radio channel on this node"; if (path.startsWith("/settings/discord")) return "Back to Discord settings"; if (path.startsWith(SETTINGS)) return "Back to organization settings"; return "Continue"; } export default function DiscordConnectedPage() { const { user, loading } = useAuth(); const router = useRouter(); const [view, setView] = useState({ kind: "working" }); const cb = useRef(undefined); const sent = useRef(false); useEffect(() => { // First run: stash code/state for this tab and strip them from the // address bar. Later runs (auth settling) read the stashed copy. if (cb.current === undefined) cb.current = captureCallback(); const c = cb.current; if (!c) { setView({ kind: "none" }); return; } if (c.error) { clearCallback(); // Only a short code from the URL is ever echoed, never free text a crafted link could carry. const code = /^[a-z_]{1,40}$/.test(c.error) ? ` (${c.error})` : ""; setView(c.error === "access_denied" ? { kind: "denied" } : { kind: "error", message: `Discord didn't finish adding the bot${code}. Nothing was changed.` }); return; } if (!c.code || !c.state) { clearCallback(); setView({ kind: "error", message: "Discord's reply was incomplete. Nothing was changed — start again from Connect a Discord server." }); return; } if (loading) return; if (!user) { // Keep the stash; /login sends the user back here once signed in. router.replace(`/login?next=${encodeURIComponent(OAUTH_CALLBACK_PATH)}`); return; } if (sent.current) return; // the code is single use — never send it twice (StrictMode, re-renders) sent.current = true; clearCallback(); completeDiscordOAuth(c.code, c.state) .then((result) => setView({ kind: "done", result })) .catch((err) => setView({ kind: "error", message: parseC2Error(err).message })); }, [loading, user, router]); return (

Connect a Discord server

); } function Body({ view }: { view: View }) { switch (view.kind) { case "working": return

Linking the server and checking the radio bot's permissions…

; case "none": return ( <>

There is nothing to finish here. Start from Connect a Discord server in your organization settings.

Organization settings ); case "denied": return ( <>

Nothing changed. The bot was not added, and no server was linked.

Back to organization settings ); case "error": return ( <>

{view.message}

If the bot did join the server, it stays there but cannot carry radio until the server is linked. Try Connect again, or use a link code.

Back to organization settings ); case "done": return ; } } function Done({ r }: { r: OAuthCompleteResult }) { const name = r.guild_name || "the server"; const next = r.return_to ?? SETTINGS; const checked = r.channels_ready !== null || r.channels_blocked !== null; const blocked = r.channels_blocked ?? 0; return ( <>

Linked {name}{r.bot_name ? <> and added {r.bot_name} : null}.

{checked ? (

{r.channels_ready ?? 0} voice channel{r.channels_ready === 1 ? "" : "s"} ready {blocked > 0 && <>, {blocked} need{blocked === 1 ? "s" : ""} a permission fix}.

) : (

The bot's channel permissions couldn't be checked yet. The node's Discord picker shows them.

)} {blocked > 0 && (

The node's Discord picker names the missing permission on each channel and how to allow it.

)} {r.stopped > 0 && (

{r.stopped} radio channel{r.stopped === 1 ? " was" : "s were"} stopped: this server used to act for the default organization.

)} {nextStepLabel(next)} ); }