Skip to content

Commit

Permalink
Merge pull request #8 from Nexite/master
Browse files Browse the repository at this point in the history
Add "Discord Information" section
  • Loading branch information
Nexite authored Feb 21, 2021
2 parents 790e90d + de8cf2c commit 5287b94
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 13 deletions.
6 changes: 6 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ module.exports = {
managementDomain: process.env.AUTH0_MANAGEMENT_DOMAIN,
hookSharedSecret: process.env.AUTH0_HOOK_SHARED_SECRET,
},
discord: {
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
redirectUri: process.env.DISCORD_REDIRECT_URI,
},
gqlAccountSecret: process.env.GQL_ACCOUNT_SECRET,
},
publicRuntimeConfig: {
appUrl: process.env.APP_URL,
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
"auth0": "^2.24.0",
"axios": "^0.19.2",
"babel-plugin-import-graphql": "^2.7.0",
"deepmerge": "^4.2.2",
"discord-oauth2": "^2.6.0",
"emotion-theming": "^10.0.27",
"graphql": "^15.4.0",
"jsonwebtoken": "^8.5.1",
"next": "^10.0.3",
"next-auth": "3.1.0",
"phone": "^2.4.8",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "16.13.1",
"deepmerge": "^4.2.2",
"phone": "^2.4.8"
"react-dom": "16.13.1"
},
"devDependencies": {
"@codeday/eslint-config": "^1.3.0",
"eslint": "^7.6.0"
}
}

5 changes: 5 additions & 0 deletions src/components/UserProperty/Discord.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation UnlinkDiscordMutation {
account {
unlinkDiscord(userId: "")
}
}
90 changes: 90 additions & 0 deletions src/components/UserProperty/Discord.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import FormControl, { Label, HelpText } from '@codeday/topo/Molecule/FormControl'
import Image from '@codeday/topo/Atom/Image';
import Box from '@codeday/topo/Atom/Box';
import Text from '@codeday/topo/Atom/Text';
import Button from '@codeday/topo/Atom/Button';
import { UnlinkDiscordMutation } from './Discord.gql'
import { tryAuthenticatedApiQuery } from '../../util/api';
import { useRouter } from 'next/router'
import { useToasts } from '@codeday/topo/utils';
import Link from '@codeday/topo/Atom/Text/Link';
import { Popover, PopoverTrigger, PopoverArrow, PopoverContent, PopoverHeader, PopoverCloseButton, PopoverBody } from '@chakra-ui/core';

const unlinkDiscord = async (token) => {
const { error } = await tryAuthenticatedApiQuery(UnlinkDiscordMutation, {}, token)
return !error ? true : false
};

const Discord = ({ user, token }) => {
const [isLinked, setIsLinked] = useState(user.discordId ? true : false)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const router = useRouter()
const { success } = useToasts();
const picture = user?.discordInformation?.avatar.endsWith("null") ? `https://cdn.discordapp.com/embed/avatars/${user.discordInformation.discriminator % 5}.png` : user?.discordInformation?.avatar || null

return (
<FormControl>
<Label fontWeight="bold">Discord Information</Label>
{isLinked ?
<Box>
<Box style={{ clear: 'both', display: "flex", alignItems: "center" }}>
<Image mb={2} src={picture} alt="" float="left" mr={2} height="2em" rounded="full" /> <Text fontSize="1em">{user.discordInformation.handle}</Text>
</Box>
<Popover isOpen={isPopoverOpen} onOpen={() => setIsPopoverOpen(true)} onClose={() => setIsPopoverOpen(false)}>
<PopoverTrigger>
<Button
size="xs"
marginRight="3"
>
Unlink Discord account
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Confirmation!</PopoverHeader>
<PopoverBody>
<p>Are you sure you want to do that?</p>
<Button size="xs"
style={{ width: "50%" }}
onClick={async () => {
await unlinkDiscord(token);
setIsLinked(false);
success("Unlinked Discord Account!")
}}>
Yes
</Button>
<Button size="xs"
variantColor="red"
style={{ width: "50%" }}
onClick={() => setIsPopoverOpen(false)}>
No
</Button>
</PopoverBody>
</PopoverContent>
</Popover>
<HelpText>
Your account is linked and ready! Make sure you are in the <Link href="https://discord.gg/codeday">CodeDay Discord Server</Link>!
</HelpText>
</Box>
: <Box>
<Button onClick={() =>
router.push("/api/discord/link")
}>
Link Discord
</Button>
<HelpText>
Link your Discord account to get full access to the <Link href="https://discord.gg/codeday">CodeDay Discord Server</Link>.
</HelpText>
</Box>}
</FormControl>
);
};
Discord.propTypes = {
user: PropTypes.object.isRequired,
token: PropTypes.string.isRequired,
};
Discord.provides = "discord";
export default Discord;
5 changes: 3 additions & 2 deletions src/components/UserProperty/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import Volunteer from './Volunteer';
import Title from './Title';
import CodeOfConduct from './CodeOfConduct';
import Badges from './Badges';
import Discord from './Discord'

export {
Username, Picture, Name, DisplayName, Pronoun, Phone, Bio, Volunteer, Title, CodeOfConduct, Badges,
Username, Picture, Name, DisplayName, Pronoun, Phone, Bio, Volunteer, Title, CodeOfConduct, Badges, Discord
};
const allExports = [Username, Picture, Name, DisplayName, Pronoun, Phone, Bio, Volunteer, Title, CodeOfConduct, Badges];
const allExports = [Username, Picture, Name, DisplayName, Pronoun, Phone, Bio, Volunteer, Title, CodeOfConduct, Badges, Discord];

const UserProperty = (fields) => {
let seenProviders = [];
Expand Down
11 changes: 11 additions & 0 deletions src/lib/discord.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import getConfig from 'next/config';
import OAuth from 'discord-oauth2'

const { serverRuntimeConfig } = getConfig();

export const discordApi = new OAuth({
version: 'v8',
clientId: serverRuntimeConfig.discord.clientId,
clientSecret: serverRuntimeConfig.discord.clientSecret,
redirectUri: serverRuntimeConfig.discord.redirectUri,
});
28 changes: 28 additions & 0 deletions src/pages/api/discord/callback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable no-console */
import { getSession } from 'next-auth/client';
import jwt from 'jsonwebtoken';
import { LinkDiscordMutation, CheckCodeDayLinked } from './discord.gql'
import { tryAuthenticatedServerApiQuery } from '../../../util/api';
import { discordApi } from '../../../lib/discord'
import getConfig from 'next/config';

const { serverRuntimeConfig } = getConfig();

export default async (req, res) => {
const code = req.query.code;
const { access_token } = await discordApi.tokenRequest({
code: code,
scope: "identify guilds",
grantType: "authorization_code",
})
const {id: discordId} = await discordApi.getUser(access_token)
const { result, error } = await tryAuthenticatedServerApiQuery(CheckCodeDayLinked, { discordId })
if (result.account.getUser) {
res.redirect('/discord/error?code=discordalreadylinked');
return
}
const userId = await (await getSession({ req })).user.id
const token = jwt.sign({scopes: "write:users"}, serverRuntimeConfig.gqlAccountSecret, {expiresIn: "1m"})
await tryAuthenticatedServerApiQuery(LinkDiscordMutation, { discordId, userId }, token)
res.redirect(`/discord/success`);
};
21 changes: 21 additions & 0 deletions src/pages/api/discord/discord.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
mutation LinkDiscordMutation ($discordId: String!, $userId: ID!) {
account {
linkDiscord(userId: $userId, discordId: $discordId)
}
}

query CheckCodeDayLinked ($discordId: String!) {
account {
getUser (where: {discordId: $discordId}, fresh: true) {
discordId
}
}
}

query CheckDiscordLinked ($userId: ID!) {
account {
getUser (where: {id: $userId}, fresh: true) {
discordId
}
}
}
18 changes: 18 additions & 0 deletions src/pages/api/discord/link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable no-console */
import { discordApi } from '../../../lib/discord'
import crypto from 'crypto'
import { getSession } from 'next-auth/client';
import { CheckDiscordLinked } from './discord.gql'
import { tryAuthenticatedServerApiQuery } from '../../../util/api';

export default async (req, res) => {
const session = await getSession({ req })
const { result } = await tryAuthenticatedServerApiQuery(CheckDiscordLinked, {userId: session.user.id})
if (result?.account?.getUser?.discordId) {
res.redirect('/discord/error?code=codedayalreadylinked');
return
}
if (!session || !session.user) { res.redirect('/'); return; }
const discordLink = discordApi.generateAuthUrl({scope: ["identify", "guilds"],state: crypto.randomBytes(16).toString("hex")})
res.redirect(discordLink);
};
40 changes: 40 additions & 0 deletions src/pages/discord/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import Page from '../../components/Page';
import Button from '@codeday/topo/Atom/Button';

const DiscordError = ({ message }) => {
return (
<Page isLoggedIn={false}>
<p>{message}</p>
<Button
size="xs"
href="/"
as="a">
Click here to return to the main page.
</Button>
</Page>
)
};

export default DiscordError;

export const getServerSideProps = async ({ req, res, query }) => {
if (query.code == "discordalreadylinked") {
return {
props: {
message: "ERROR: That Discord account is already linked to a CodeDay account!"
}
}
} else if (query.code == "codedayalreadylinked") {
return {
props: {
message: "ERROR: Your CodeDay account is already linked to a Discord account!"
}
}
} else {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { props: {} }
}
};
15 changes: 15 additions & 0 deletions src/pages/discord/success.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import Page from '../../components/Page';
import Button from '@codeday/topo/Atom/Button';

export default () => (
<Page isLoggedIn={false}>
<p>Discord account successfuly linked!</p>
<Button
size="xs"
href="/"
as="a">
Click here to return to the main page.
</Button>
</Page>
);
6 changes: 6 additions & 0 deletions src/pages/index.gql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ query IndexUserQuery {
title
}
acceptTos
discordId
discordInformation {
discriminator
handle
avatar
}
}
}
}
4 changes: 2 additions & 2 deletions src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export default function Home({ user, token, logIn }) {
<WelcomeHeader user={user}></WelcomeHeader>
<Divider />
<Heading as="h2" size="lg" paddingTop={4}>Update Your Account</Heading>
<UserProperties token={token} user={merge(user, changes)} fields={["username", "picture", 'familyName', 'givenName', "displayNameFormat", "pronoun", (user.badges ? "badges" : null), "phoneNumber", "bio", "volunteer", (user.roles.find((role) => role.name === "Volunteer") ? "title" : null),]} onChange={setChanges}></UserProperties>
<UserProperties token={token} user={merge(user, changes)} fields={["username", "picture", 'familyName', 'givenName', "displayNameFormat", "pronoun", (user.badges ? "badges" : null), "phoneNumber", "bio", "volunteer", (user.roles.find((role) => role.name === "Volunteer") ? "title" : null), "discord"]} onChange={setChanges}></UserProperties>
<Box textAlign="right">
<SubmitUpdates token={token} signedIn={true} user={user} changes={changes} required={['username', 'givenName', 'familyName', 'pronoun']} onSubmit={onSubmit} />
<SubmitUpdates token={token} user={user} changes={changes} required={['username', 'givenName', 'familyName', 'pronoun']} onSubmit={onSubmit} />
<Box marginTop="3">
<Button
as="a"
Expand Down
14 changes: 14 additions & 0 deletions src/util/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ export async function tryAuthenticatedApiQuery(gql, params, token) {
return { error: err };
}
}

export async function tryAuthenticatedServerApiQuery(gql, params, token) {
const headers = {
'Authorization': `Bearer ${token}`,
};

try {
return {
result: await apiFetch(print(gql), params || {}, token ? headers : {}),
};
} catch (err) {
return { error: err };
}
}
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"

discord-oauth2@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/discord-oauth2/-/discord-oauth2-2.6.0.tgz#d362b386e77a3eb3135772307a253514e0dc48be"
integrity sha512-9lEt1Rp9rruFPiVQOPbe2rKJXB40fO2u9Ozjz3n/TlVYnIxCTeohVkNI/c5AoLFDZcSxFc2MtDTf9iG0X7BkRg==

[email protected]:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
Expand Down Expand Up @@ -4702,11 +4707,6 @@ [email protected]:
dependencies:
he "1.2.0"

node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==

node-libs-browser@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
Expand Down

0 comments on commit 5287b94

Please sign in to comment.