mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-08 03:05:16 -05:00
feat: basic discord connection support
This commit is contained in:
parent
6abdcc718e
commit
0f5da1dba1
|
|
@ -26,11 +26,28 @@ export default defineNuxtConfig({
|
|||
runtimeConfig: {
|
||||
apiBase: 'https://api.pretendo.cc',
|
||||
|
||||
discord: {
|
||||
botToken: '',
|
||||
clientID: '',
|
||||
clientSecret: ''
|
||||
},
|
||||
|
||||
public: {
|
||||
hCaptchaSitekey: ''
|
||||
}
|
||||
},
|
||||
|
||||
hooks: {
|
||||
'pages:extend'(pages) {
|
||||
// add a route
|
||||
pages.push({
|
||||
name: 'connect',
|
||||
path: '/account/connect/:connection',
|
||||
file: '~/pages/account/index.vue'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
fonts: {
|
||||
|
|
|
|||
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -16,6 +16,8 @@
|
|||
"@nuxt/fonts": "^0.11.1",
|
||||
"@nuxt/icon": "^1.11.0",
|
||||
"@nuxtjs/i18n": "^9.5.3",
|
||||
"discord-api-types": "^0.38.8",
|
||||
"discord-oauth2": "^2.12.1",
|
||||
"eslint": "^9.24.0",
|
||||
"feed": "^4.2.2",
|
||||
"nuxt": "^3.16.2",
|
||||
|
|
@ -24,6 +26,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@pretendonetwork/eslint-config": "^0.0.8",
|
||||
"@types/node": "^22.15.21",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"sass-embedded": "^1.86.3"
|
||||
}
|
||||
|
|
@ -3913,6 +3916,16 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||
|
|
@ -6449,6 +6462,12 @@
|
|||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord-oauth2": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz",
|
||||
"integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
|
|
@ -15046,6 +15065,13 @@
|
|||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz",
|
||||
|
|
|
|||
|
|
@ -20,14 +20,20 @@
|
|||
"@nuxt/fonts": "^0.11.1",
|
||||
"@nuxt/icon": "^1.11.0",
|
||||
"@nuxtjs/i18n": "^9.5.3",
|
||||
"discord-api-types": "^0.38.8",
|
||||
"discord-oauth2": "^2.12.1",
|
||||
"eslint": "^9.24.0",
|
||||
"feed": "^4.2.2",
|
||||
"nuxt": "^3.16.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pretendonetwork/eslint-config": "^0.0.8",
|
||||
"@types/node": "^22.15.21",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"sass-embedded": "^1.86.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,49 @@ interface InfoCCResponse {
|
|||
};
|
||||
}
|
||||
|
||||
interface DiscordInfoResponse {
|
||||
username?: string;
|
||||
authLink?: string;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const account = await useFetch<InfoCCResponse>('/api/account/info'); // TODO: easy way to share this between navbar and account page? is that a good idea?
|
||||
|
||||
const showingEditingDisabledModal = ref(false);
|
||||
|
||||
if (route.params.connection) { // my nightmares are made of this logic
|
||||
if (import.meta.server) {
|
||||
switch (route.params.connection) { // future proofing for more connections?
|
||||
case 'discord': {
|
||||
if (route.query.code) {
|
||||
await useFetch('/api/account/connections/discord/add', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
code: route.query.code
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
router.replace('/account');
|
||||
}
|
||||
|
||||
const discordInfo = await useFetch<DiscordInfoResponse>('/api/account/connections/discord/info');
|
||||
|
||||
async function handleUnlinkDiscord() {
|
||||
try {
|
||||
await $fetch('/api/account/connections/discord/remove', { method: 'DELETE' });
|
||||
discordInfo.refresh();
|
||||
} catch {
|
||||
console.log('uhoh');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -333,17 +373,23 @@ const showingEditingDisabledModal = ref(false);
|
|||
<h2 class="header">
|
||||
{{ $t("account.settings.settingCards.discord") }}
|
||||
</h2>
|
||||
<!--{{#if discordUser}}
|
||||
<p>{{ locale.account.settings.settingCards.connectedToDiscord }} {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
|
||||
<a href="/account/remove/discord">
|
||||
<button
|
||||
id="remove-discord-connection"
|
||||
class="button secondary"
|
||||
>{{ locale.account.settings.settingCards.removeDiscord }}</button>
|
||||
</a>
|
||||
{{else}}
|
||||
<p>{{ locale.account.settings.settingCards.noDiscordLinked }} <a href="{{ discordAuthURL }}">{{ locale.account.settings.settingCards.linkDiscord }}</a></p>
|
||||
{{/if}}-->
|
||||
<template
|
||||
v-if="discordInfo.data.value?.username"
|
||||
>
|
||||
<p>{{ $t("account.settings.settingCards.connectedToDiscord") }} {{ discordInfo.data.value.username }}</p>
|
||||
<button
|
||||
id="remove-discord-connection"
|
||||
class="button secondary"
|
||||
@click="handleUnlinkDiscord"
|
||||
>
|
||||
{{ $t("account.settings.settingCards.removeDiscord") }}
|
||||
</button>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<p>{{ $t("account.settings.settingCards.noDiscordLinked") }} <a :href="discordInfo.data.value?.authLink">{{ $t("account.settings.settingCards.linkDiscord") }}</a></p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-card">
|
||||
|
|
@ -481,7 +527,7 @@ const showingEditingDisabledModal = ref(false);
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: no-repeat center/40% url("/assets/images/edit.svg"), rgba(55, 60, 101, 0.7);
|
||||
background: no-repeat center/40% /*url("/assets/images/edit.svg"),*/ rgba(55, 60, 101, 0.7);
|
||||
opacity: 0;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
|
|
|||
49
src/server/api/account/connections/discord/add/index.ts
Normal file
49
src/server/api/account/connections/discord/add/index.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import DiscordOauth2 from 'discord-oauth2';
|
||||
import { FetchError } from 'ofetch';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const cookies = parseCookies(event);
|
||||
|
||||
const discordOauth = new DiscordOauth2({
|
||||
clientId: useRuntimeConfig(event).discord.clientID,
|
||||
clientSecret: useRuntimeConfig(event).discord.clientSecret,
|
||||
redirectUri: `http://localhost:3000/account/connect/discord`,
|
||||
version: 'v10'
|
||||
});
|
||||
|
||||
try {
|
||||
// Attempt to get OAuth2 tokens
|
||||
const tokens = await discordOauth.tokenRequest({
|
||||
code: body.code,
|
||||
scope: 'identify guilds',
|
||||
grantType: 'authorization_code'
|
||||
});
|
||||
|
||||
const discordUser = await discordOauth.getUser(tokens.access_token);
|
||||
|
||||
await $fetch(`/v1/connections/add/discord`, {
|
||||
method: 'POST',
|
||||
baseURL: useRuntimeConfig(event).apiBase,
|
||||
headers: { Authorization: `${cookies?.token_type} ${cookies?.access_token}` },
|
||||
body: {
|
||||
data: {
|
||||
id: discordUser.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true }; // TODO: figure out why $fetch doesn't send cookies but useFetch does so this dumb fix can be removed
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
if (error instanceof FetchError) {
|
||||
setResponseStatus(event, error.status, error.data.error);
|
||||
} else {
|
||||
setResponseStatus(event, 500);
|
||||
}
|
||||
|
||||
event.node.res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
76
src/server/api/account/connections/discord/info/index.ts
Normal file
76
src/server/api/account/connections/discord/info/index.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import * as crypto from 'crypto';
|
||||
import { REST as DiscordRest } from '@discordjs/rest';
|
||||
import { Routes } from 'discord-api-types/v10';
|
||||
import { FetchError } from 'ofetch';
|
||||
import DiscordOauth2 from 'discord-oauth2';
|
||||
import type { APIUser } from 'discord-api-types/v10';
|
||||
|
||||
interface InfoCCResponse { // TODO: de-dupe
|
||||
username: string;
|
||||
access_level: number;
|
||||
mii: {
|
||||
name: string;
|
||||
image_url: string;
|
||||
};
|
||||
connections: {
|
||||
stripe?: {
|
||||
tier_name?: string;
|
||||
tier_level?: string;
|
||||
};
|
||||
discord?: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
birthdate: string;
|
||||
gender: string;
|
||||
country: string;
|
||||
timezone: {
|
||||
name: string;
|
||||
};
|
||||
server_access_level: string;
|
||||
email: {
|
||||
address: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const discordRest = new DiscordRest({ version: '10' }).setToken(useRuntimeConfig(event).discord.botToken);
|
||||
const discordOauth = new DiscordOauth2({
|
||||
clientId: useRuntimeConfig(event).discord.clientID,
|
||||
clientSecret: useRuntimeConfig(event).discord.clientSecret,
|
||||
redirectUri: `http://localhost:3000/account/connect/discord`,
|
||||
version: 'v10'
|
||||
});
|
||||
|
||||
try {
|
||||
const cookies = parseCookies(event);
|
||||
|
||||
const accountInfo = await $fetch<InfoCCResponse>(`/v1/user`, {
|
||||
method: 'GET',
|
||||
baseURL: useRuntimeConfig(event).apiBase,
|
||||
headers: { Authorization: `${cookies?.token_type} ${cookies?.access_token}` }
|
||||
});
|
||||
|
||||
if (!accountInfo.connections.discord?.id) {
|
||||
return {
|
||||
authLink: discordOauth.generateAuthUrl({
|
||||
scope: ['identify', 'guilds'],
|
||||
state: crypto.randomBytes(16).toString('hex')
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return await discordRest.get(Routes.user(accountInfo.connections.discord?.id)) as APIUser;
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
if (error instanceof FetchError) {
|
||||
setResponseStatus(event, error.status, error.data.error);
|
||||
} else {
|
||||
setResponseStatus(event, 500);
|
||||
}
|
||||
|
||||
event.node.res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
24
src/server/api/account/connections/discord/remove/index.ts
Normal file
24
src/server/api/account/connections/discord/remove/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { FetchError } from 'ofetch';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const cookies = parseCookies(event);
|
||||
|
||||
try {
|
||||
await $fetch(`/v1/connections/remove/discord`, {
|
||||
method: 'DELETE',
|
||||
baseURL: useRuntimeConfig(event).apiBase,
|
||||
headers: { Authorization: `${cookies?.token_type} ${cookies?.access_token}` }
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
if (error instanceof FetchError) {
|
||||
setResponseStatus(event, error.status, error.data.error);
|
||||
} else {
|
||||
setResponseStatus(event, 500);
|
||||
}
|
||||
|
||||
event.node.res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user