Skip to content

Commit

Permalink
Merge pull request #101 from communitycenter/discord-confirmation
Browse files Browse the repository at this point in the history
Feature: Add pop-up before joining Discord
  • Loading branch information
jacc authored May 2, 2024
2 parents 5d93308 + ee0fe9b commit 877e886
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 44 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@planetscale/database": "^1.10.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.5",
Expand Down
71 changes: 71 additions & 0 deletions src/components/dialogs/login-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Link from "next/link";

import Image from "next/image";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
} from "@/components/ui/dialog";
import { useState } from "react";

interface Props {
open: boolean;
setOpen: (open: boolean) => void;
}

export const LoginDialog = ({ open, setOpen }: Props) => {
const [joinDiscord, setJoinDiscord] = useState(true);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<div className="flex justify-center">
<Image src="/discord.png" alt={"Heart icon"} width={48} height={48} />
</div>
<DialogHeader>
<DialogDescription>
stardew.app uses Discord to sync your saves, preferences, and stats
across devices - nothing else!
</DialogDescription>
<DialogDescription>
<div className="items-top flex space-x-2 pt-2 text-left">
<Checkbox
id="ccdiscord"
defaultChecked={joinDiscord}
onCheckedChange={(checked) => setJoinDiscord(Boolean(checked))}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="ccdiscord"
className="text-sm font-medium leading-none text-black peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-white"
>
Join the Community Center Discord
</label>
<p className="text-muted-foreground text-sm">
We occasionally post updates, information and more in our
Discord server. You can leave at any time, no hard feelings!
</p>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-3 sm:gap-0">
<Button>
<Link
href={{
pathname: "/api/oauth",
query: { discord: joinDiscord },
}}
>
Log In With Discord
</Link>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
12 changes: 10 additions & 2 deletions src/components/sheets/mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface Props {
setCreditsOpen: Dispatch<SetStateAction<boolean>>;
setFeedbackOpen: Dispatch<SetStateAction<boolean>>;
setBugreportOpen: Dispatch<SetStateAction<boolean>>;
setLoginOpen: Dispatch<SetStateAction<boolean>>;

inputRef: MutableRefObject<HTMLInputElement | null>;
}
Expand All @@ -52,6 +53,7 @@ export const MobileNav = ({
setCreditsOpen,
setFeedbackOpen,
setBugreportOpen,
setLoginOpen,
}: Props) => {
const api = useSWR<User>(
"/api",
Expand Down Expand Up @@ -125,8 +127,14 @@ export const MobileNav = ({
<div className="grid grid-cols-1 gap-2">
{!api.data?.discord_id && (
<>
<Button className="hover:bg-[#5865F2] dark:hover:bg-[#5865F2] dark:hover:text-white">
<Link href="/api/oauth">Log In With Discord</Link>
<Button
className="hover:bg-[#5865F2] dark:hover:bg-[#5865F2] dark:hover:text-white"
data-umami-event="Log in"
onClick={() => {
setLoginOpen(true);
}}
>
Log In with Discord
</Button>

<Button
Expand Down
9 changes: 8 additions & 1 deletion src/components/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { toast } from "sonner";
import { BugReportDialog } from "./dialogs/bugreport-dialog";
import { ChangelogDialog } from "./dialogs/changelog-dialog";
import { FeedbackDialog } from "./dialogs/feedback-dialog";
import { LoginDialog } from "./dialogs/login-dialog";
import { UploadDialog } from "./dialogs/upload-dialog";

export interface User {
Expand All @@ -57,6 +58,7 @@ export function Topbar() {
const [feedbackOpen, setFeedbackOpen] = useState(false);
const [changelogOpen, setChangelogOpen] = useState(false);
const [bugreportOpen, setBugreportOpen] = useState(false);
const [loginOpen, setLoginOpen] = useState(false);
const [uploadOpen, setUploadOpen] = useState(false);

const [isDevelopment, setIsDevelopment] = useState(false);
Expand Down Expand Up @@ -145,8 +147,11 @@ export function Topbar() {
<Button
className="hover:bg-[#5865F2] dark:hover:bg-[#5865F2] dark:hover:text-white"
data-umami-event="Log in"
onClick={() => {
setLoginOpen(true);
}}
>
<Link href="/api/oauth">Log In With Discord</Link>
Log In with Discord
</Button>
)}
{/* Logged In */}
Expand Down Expand Up @@ -264,13 +269,15 @@ export function Topbar() {
setFeedbackOpen={setFeedbackOpen}
setCreditsOpen={setCreditsOpen}
setBugreportOpen={setBugreportOpen}
setLoginOpen={setLoginOpen}
inputRef={inputRef}
/>
<CreditsDialog open={creditsOpen} setOpen={setCreditsOpen} />
<DeletionDialog open={deletionOpen} setOpen={setDeletionOpen} />
<FeedbackDialog open={feedbackOpen} setOpen={setFeedbackOpen} />
<ChangelogDialog open={changelogOpen} setOpen={setChangelogOpen} />
<BugReportDialog open={bugreportOpen} setOpen={setBugreportOpen} />
<LoginDialog open={loginOpen} setOpen={setLoginOpen} />
<UploadDialog open={uploadOpen} setOpen={setUploadOpen} />
</>
);
Expand Down
28 changes: 28 additions & 0 deletions src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"

import { cn } from "@/lib/utils"

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-800 dark:border-neutral-50 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
44 changes: 24 additions & 20 deletions src/pages/api/oauth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Data = Record<string, any>;

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
try {
// get state from cookie to verify that this is the correct authentication request
Expand All @@ -21,20 +21,22 @@ export default async function handler(
const uid = getCookie("uid", { req });
if (!uid) {
res.status(400).end();
res.redirect("/");
console.log("[OAuth] No UID cookie");
return;
}

const code = req.query.code as string;
if (!code) {
res.status(400).end();
res.redirect("/");
console.log("[OAuth] No code");
return;
}

const discord = await fetch(
`https://discord.com/api/oauth2/token?grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(
process.env.DISCORD_REDIRECT ?? ""
process.env.DISCORD_REDIRECT ?? "",
)}`,
{
method: "POST",
Expand All @@ -48,7 +50,7 @@ export default async function handler(
code: code ?? "",
redirect_uri: process.env.DISCORD_REDIRECT ?? "",
}),
}
},
);

if (!discord.ok) {
Expand Down Expand Up @@ -95,15 +97,15 @@ export default async function handler(
if (discordUser.discord_name !== discordUserData.username) {
const r = await conn.execute(
"UPDATE Users SET discord_name = ? WHERE discord_id = ?",
[discordUserData.username, discordUserData.id]
[discordUserData.username, discordUserData.id],
);
}

// update discord avatar if the avatar hash changed
if (discordUser.discord_avatar !== discordUserData.avatar) {
const r = await conn.execute(
"UPDATE Users SET discord_avatar = ? WHERE discord_id = ?",
[discordUserData.avatar, discordUserData.id]
[discordUserData.avatar, discordUserData.id],
);
}
} else {
Expand All @@ -115,7 +117,7 @@ export default async function handler(
discordUserData.username,
discordUserData.avatar,
cookieSecret,
]
],
);
user = {
id: uid as string,
Expand Down Expand Up @@ -160,25 +162,27 @@ export default async function handler(
? "localhost"
: "stardew.app",
expires: new Date(token.expires * 1000),
}
},
);

res.redirect("/");

const addToGuild = await fetch(
`https://discord.com/api/guilds/${process.env.DISCORD_GUILD}/members/${discordUserData.id}`,
{
method: "PUT",
body: JSON.stringify({
access_token: `${discordData.access_token}`,
roles: ["1150490180860530819"],
}),
headers: {
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
"Content-Type": "application/json",
if (discordData.scope.includes("guilds.join")) {
await fetch(
`https://discord.com/api/guilds/${process.env.DISCORD_GUILD}/members/${discordUserData.id}`,
{
method: "PUT",
body: JSON.stringify({
access_token: `${discordData.access_token}`,
roles: ["1150490180860530819"],
}),
headers: {
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
"Content-Type": "application/json",
},
},
}
);
);
}
} catch (e: any) {
res.status(500).send(e.message);
console.log("[OAuth] Error", e);
Expand Down
7 changes: 4 additions & 3 deletions src/pages/api/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ type Data = Record<string, any>;

export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
console.log(!req.query.discord ? `&guilds.join` : ``);
const state = crypto.randomBytes(4).toString("hex");
setCookie("oauth_state", state, {
req,
Expand All @@ -21,7 +22,7 @@ export default function handler(
`https://discord.com/api/oauth2/authorize?client_id=${
process.env.DISCORD_ID
}&redirect_uri=${encodeURIComponent(
process.env.DISCORD_REDIRECT ?? ""
)}&state=${state}&response_type=code&scope=identify%20guilds.join`
process.env.DISCORD_REDIRECT ?? "",
)}&state=${state}&response_type=code&scope=identify${req.query && !req.query.discord ? `` : `%20guilds.join`}`,
);
}
Loading

0 comments on commit 877e886

Please sign in to comment.