Skip to content

Commit df3daa6

Browse files
committed
Streamline role setup and allow override
1 parent 82f5762 commit df3daa6

File tree

9 files changed

+226
-25
lines changed

9 files changed

+226
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ docker-compose-staging.yml
8686
apps/webstack/.vercel
8787
.turbo
8888
apps/webstack/.vercel
89+
.wrangler

apps/webstack/src/app.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ interface SessionData {
2929
discord?: {
3030
id?: string;
3131
state?: string;
32+
accessToken?: string;
3233
}
3334
error?: string;
35+
isReady?: boolean;
3436
}
3537

apps/webstack/src/main.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,18 @@ a {
9999
.text-centered {
100100
text-align: center;
101101
}
102+
103+
.spinner {
104+
width: 56px;
105+
height: 56px;
106+
border: 6px solid rgba(0,0,0,0.15);
107+
border-top-color: #5865f2;
108+
border-radius: 50%;
109+
animation: spin 1s linear infinite;
110+
margin: 1rem auto 2rem auto;
111+
}
112+
113+
@keyframes spin {
114+
0% { transform: rotate(0deg); }
115+
100% { transform: rotate(360deg); }
116+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { RequestHandler } from './$types';
2+
import { config } from 'config';
3+
import { env } from '$env/dynamic/private';
4+
import { logger } from '$lib/logger';
5+
import { BotResult } from '$lib/DiscordTypes';
6+
7+
enum MemberResult {
8+
Found,
9+
NotFound,
10+
Error
11+
}
12+
13+
async function getGuildMember(id: string) {
14+
try {
15+
const response = await fetch(`https://discord.com/api/v10/guilds/${config.discord.guildId}/members/${id}`, {
16+
method: 'GET',
17+
headers: {
18+
"Content-Type": "application/json",
19+
"Authorization": `Bot ${env.DISCORD_BOT_TOKEN}`
20+
},
21+
});
22+
23+
const json = await response.json();
24+
switch (response.status) {
25+
case 200:
26+
return { content: json, result: MemberResult.Found } as const;
27+
case 404:
28+
logger.info(`User ${id} not found in guild.`);
29+
return { content: json, result: MemberResult.NotFound } as const;
30+
default:
31+
logger.error(`Error checking if user ${id} exists in guild: ${response.status}: ${response.statusText}`);
32+
return { content: json, result: MemberResult.Error } as const;
33+
}
34+
} catch (e) {
35+
logger.error(e);
36+
return { content: { code: -5000, message: 'Network error' }, result: MemberResult.Error } as const;
37+
}
38+
}
39+
40+
async function joinDiscordServer(userId: string, accessToken: string, nickname: string) {
41+
try {
42+
const response = await fetch(`https://discord.com/api/v10/guilds/${config.discord.guildId}/members/${userId}`, {
43+
body: JSON.stringify({
44+
access_token: accessToken,
45+
nick: nickname,
46+
roles: config.discord.roles.map((val) => val.id)
47+
}),
48+
method: 'PUT',
49+
headers: {
50+
"Content-Type": "application/json",
51+
"Authorization": `Bot ${env.DISCORD_BOT_TOKEN}`
52+
},
53+
});
54+
55+
switch (response.status) {
56+
case 201:
57+
case 204:
58+
return { result: BotResult.Success, error: null } as const;
59+
case 400:
60+
return { result: BotResult.Full, error: await response.json() } as const;
61+
default:
62+
return { result: BotResult.Error, error: await response.json() } as const;
63+
}
64+
} catch (e) {
65+
logger.error(e);
66+
return { result: BotResult.Error, error: { code: -5000, message: 'Network Error' } } as const;
67+
}
68+
}
69+
70+
async function addRoleToUser(userId: string, roles: string[], nick: string) {
71+
const response = await fetch(`https://discord.com/api/v10/guilds/${config.discord.guildId}/members/${userId}`, {
72+
body: JSON.stringify({ nick, roles }),
73+
method: 'PATCH',
74+
headers: {
75+
"Content-Type": "application/json",
76+
"Authorization": `Bot ${env.DISCORD_BOT_TOKEN}`
77+
},
78+
});
79+
80+
switch (response.status) {
81+
case 200:
82+
case 204:
83+
return { result: BotResult.Success, error: null } as const;
84+
default:
85+
return { result: BotResult.Error, error: await response.json() } as const;
86+
}
87+
}
88+
89+
export const POST = (async ({ locals }) => {
90+
const discordId = locals.session.data.discord?.id;
91+
const accessToken = locals.session.data.discord?.accessToken;
92+
const osuUsername = locals.session.data.osu?.username ?? '';
93+
94+
if (!discordId || !accessToken) {
95+
return new Response(JSON.stringify({ result: 'error', message: 'Missing session data.' }), { status: 400 });
96+
}
97+
98+
const memberCheck = await getGuildMember(discordId);
99+
if (memberCheck.result === MemberResult.Found) {
100+
logger.info(`User ${discordId} already exists in the guild. Adding roles...`);
101+
const requiredRoles = config.discord.roles.map((val) => val.id);
102+
const guildMember = memberCheck.content as { roles: string[] };
103+
const mergedRoles = Array.from(new Set([...(guildMember.roles || []), ...requiredRoles]));
104+
const unchanged = mergedRoles.length === (guildMember.roles || []).length && (guildMember.roles || []).every((id: string) => mergedRoles.includes(id));
105+
if (unchanged) {
106+
await locals.session.update((d) => { (d as any).isReady = true; return d; });
107+
return new Response(JSON.stringify({ result: 'success' }));
108+
}
109+
const addRoleRes = await addRoleToUser(discordId, requiredRoles, osuUsername);
110+
if (addRoleRes.result === BotResult.Success) {
111+
await locals.session.update((d) => { (d as any).isReady = true; return d; });
112+
return new Response(JSON.stringify({ result: 'success' }));
113+
}
114+
return new Response(JSON.stringify({ result: 'error', message: addRoleRes.error?.message || 'Failed adding roles' }), { status: 500 });
115+
}
116+
117+
if (memberCheck.result === MemberResult.NotFound) {
118+
const joinRes = await joinDiscordServer(discordId, accessToken, osuUsername);
119+
if (joinRes.result === BotResult.Full) {
120+
await locals.session.update((d) => { d.error = 'You have joined the maximum amount of servers.'; return d; });
121+
return new Response(JSON.stringify({ result: 'full' }), { status: 200 });
122+
}
123+
if (joinRes.result !== BotResult.Success) {
124+
return new Response(JSON.stringify({ result: 'error', message: joinRes.error?.message || 'Failed to join server' }), { status: 500 });
125+
}
126+
const requiredRoles = config.discord.roles.map((val) => val.id);
127+
const addRoleRes = await addRoleToUser(discordId, requiredRoles, osuUsername);
128+
if (addRoleRes.result === BotResult.Success) {
129+
await locals.session.update((d) => { (d as any).isReady = true; return d; });
130+
return new Response(JSON.stringify({ result: 'success' }));
131+
}
132+
return new Response(JSON.stringify({ result: 'error', message: addRoleRes.error?.message || 'Failed adding roles' }), { status: 500 });
133+
}
134+
135+
return new Response(JSON.stringify({ result: 'error', message: 'Discord API error' }), { status: 502 });
136+
}) satisfies RequestHandler;
137+
138+

apps/webstack/src/routes/auth/discord/callback/+server.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -342,29 +342,16 @@ export const GET = (async ({ url, locals }) => {
342342
return data;
343343
});
344344

345-
const { result, error } = await setupUser(meData.user, tokens.access_token, locals.session.data.osu?.username ?? '');
346-
logger.info(`User ${meData.user.id} ${meData.user.username}#${meData.user.discriminator} received: ${BotResult[result]}`);
347-
348-
if (result === BotResult.Full) {
349-
await locals.session.update((data) => {
350-
data.error = "You have joined the maxmium amount of servers. Please leave a server before trying to rejoin this one."
351-
return data;
352-
});
353-
354-
redirect(302, '/');
355-
} else if (result === BotResult.Error) {
356-
logger.error(`Redirecting user due to API side error: ${error?.code}; ${error?.message}`);
357-
await locals.session.update((data) => {
358-
data.error = "An unknown error occured while trying to join the server."
359-
return data;
360-
});
361-
362-
redirect(302, '/');
363-
}
364-
365-
sendMessageToWelcomeChannel(locals.session.data);
345+
await locals.session.update((data) => {
346+
if (!data.discord)
347+
data.discord = {};
348+
data.discord.id = meData.user.id;
349+
(data.discord as any).accessToken = tokens.access_token;
350+
(data as any).isReady = false;
351+
return data;
352+
});
366353

367-
logger.info(`Discord User joined: ${meData.user.id} - ${meData.user.username}`);
354+
logger.info(`Prepared setup for user ${meData.user.username} ${meData.user.id}`);
368355

369-
redirect(302, '/done');
356+
redirect(302, '/loading');
370357
}) satisfies RequestHandler;

apps/webstack/src/routes/checks/manual/+page.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
<div id="start">
1212
<div>
1313
<h1>Manual verification required</h1>
14-
<p>Please ping the @Moderators role in the verification channel with a screenshot of this page.</p>
15-
<p>Reason: { data.reason }</p>
14+
<p>Please ping the @Moderators role in the verification channel with the following text: </p>
15+
<p>The reason I require manual verification is: { data.reason }</p>
16+
<p style="margin-top: 0.75rem; font-size: 0.9rem; opacity: 0.3;">
17+
<a href="/checks/discord" aria-label="Proceed to verification anyway" style="color: inherit; text-decoration: underline;">I understand this server is for staff only and not players — verify me anyway</a>
18+
</p>
1619
</div>
1720
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const load = (async ({ locals }) => {
2+
await locals.session.destroy()
3+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { redirect } from '@sveltejs/kit'
2+
3+
export const load = (async ({ locals }) => {
4+
if (!locals.session.data.osu?.id || !locals.session.data.discord?.id) {
5+
redirect(302, '/')
6+
}
7+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { goto } from '$app/navigation';
4+
5+
let errorMessage: string = '';
6+
7+
onMount(async () => {
8+
await new Promise((r) => setTimeout(r, 450));
9+
try {
10+
const res = await fetch('/api/discord/setup', {
11+
method: 'POST'
12+
});
13+
if (res.ok) {
14+
const data = await res.json();
15+
if (data.result === 'success') {
16+
await new Promise((r) => setTimeout(r, 350));
17+
goto('/done');
18+
return;
19+
}
20+
if (data.result === 'full') {
21+
errorMessage = 'You have joined the maximum number of servers. Please leave one and try again.';
22+
return;
23+
}
24+
errorMessage = data.message || 'An unknown error occurred. Please try again.';
25+
return;
26+
}
27+
errorMessage = 'Unable to contact the server. Please refresh this page.';
28+
} catch (e) {
29+
errorMessage = 'Network error. Please check your connection and try again.';
30+
}
31+
});
32+
</script>
33+
34+
<div class="start">
35+
<div class="text-centered">
36+
<div class="spinner"></div>
37+
<h1>Finishing up your Discord setup…</h1>
38+
<p>Please wait while we set up your roles and join you to the server.</p>
39+
{#if errorMessage}
40+
<p style="color:#FF4C4C; font-weight:bold;">{errorMessage}</p>
41+
{/if}
42+
</div>
43+
</div>
44+
45+

0 commit comments

Comments
 (0)