Skip to content

Commit

Permalink
discord support
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherJMiller committed Jun 4, 2024
1 parent 1f36d05 commit 18f097b
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"js/ts.implicitProjectConfig.checkJs": true
}
13 changes: 13 additions & 0 deletions api/migrations/1717475269376-AddDiscordToConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDiscordToConnection1717475269376 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "connection" ADD COLUMN "discordId" VARCHAR;`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "connection" DROP COLUMN "discordId";`);
}
}
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"discord.js": "^14.15.3",
"generate-password": "^1.7.1",
"jwks-rsa": "^3.1.0",
"passport": "^0.7.0",
Expand Down
35 changes: 32 additions & 3 deletions api/src/connections/connection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiBody } from '@nestjs/swagger';
import { authenticateAgainstMinecraft } from 'src/utils/minecraft';
import { User } from 'src/users/user.entity';
import { MinecraftToken } from './connection.entity';
import { Token } from './connection.entity';
import { getDiscordId } from 'src/utils/discord';

@Controller()
export class ConnectionController {
Expand All @@ -22,11 +23,11 @@ export class ConnectionController {
@ApiBearerAuth()
@ApiBody({
required: true,
type: MinecraftToken,
type: Token,
description: 'payload containing the token',
})
async addMinecraft(
@Body() tokenPayload: MinecraftToken,
@Body() tokenPayload: Token,
@Request() req,
): Promise<void> {
const user = User.fromJwt(req.user);
Expand All @@ -43,4 +44,32 @@ export class ConnectionController {
minecraft_uuid: null,
});
}


@Post('connections/discord')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
@ApiBody({
required: true,
type: Token,
description: 'payload containing the token',
})
async addDiscord(
@Body() tokenPayload: Token,
@Request() req,
): Promise<void> {
const user = User.fromJwt(req.user);
const id = await getDiscordId(tokenPayload.token);
await this.connectionService.saveForUser(user, { discordId: id });
}

@Delete('connections/discord')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
async removeDiscord(@Request() req): Promise<void> {
const user = User.fromJwt(req.user);
await this.connectionService.saveForUser(user, {
discordId: null,
});
}
}
5 changes: 4 additions & 1 deletion api/src/connections/connection.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';

export class MinecraftToken {
export class Token {
token: string;
}

Expand All @@ -11,4 +11,7 @@ export class Connection {

@Column({ unique: true, nullable: true })
minecraft_uuid?: string;

@Column({ unique: true, nullable: true })
discordId?: string;
}
11 changes: 11 additions & 0 deletions api/src/utils/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { REST, RESTGetAPIUserResult, Routes } from "discord.js";

export async function getDiscordId(
accessToken: string,
): Promise<string> {
const api = new REST({ version: '10', authPrefix: 'Bearer' }).setToken(accessToken);

const user: RESTGetAPIUserResult = await (api.get(Routes.user()) as Promise<RESTGetAPIUserResult>);

return user.id;
}
5 changes: 5 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UserUpdate } from './pages/UserUpdate';
import { ProfilePage } from './pages/Profile';
import { MinecraftConnection } from './pages/MinecraftConnection';
import { MinecraftContextProvider } from './contexts/MinecraftContext';
import { DiscordConnection } from './pages/DiscordConnection';

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -49,6 +50,10 @@ const router = createBrowserRouter([
path: 'minecraft',
element: <MinecraftConnection />,
},
{
path: 'discord',
element: <DiscordConnection />,
},
],
},
]);
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/pages/DiscordConnection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useContext, useEffect, useState } from 'react';
import { AuthContext } from '../contexts/AuthContext';
import { Spinner } from 'flowbite-react';
import { useNavigate } from 'react-router-dom';
import { addDiscordToUser } from '../util/api';

export function DiscordConnection() {
const { token, profile } = useContext(AuthContext);
const [accessToken, setAccessToken] = useState<string | undefined>(
undefined,
);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');

const navigate = useNavigate();

useEffect(() => {
const hash = window.location.hash.substring(1);
const fragments = hash.split('&');
const accessToken = fragments.find((x) => x.includes('access_token='));

if (accessToken) {
setAccessToken(accessToken.replace('access_token=', ''));
}
}, [setAccessToken]);

useEffect(() => {
if (token && accessToken && !loading && !message) {
setLoading(true);
const run = async () => {
try {
console.log('Sending', accessToken);
const { error } = await addDiscordToUser(token, {
token: accessToken,
});

if (error) {
setMessage(JSON.stringify(error));
return;
}

setMessage('Discord Connection Complete');
navigate(`/user/${profile?.username}/edit`);
} catch (e) {
setMessage(`An error occured while connecting, ${e}`);
} finally {
setLoading(false);
}
};

run();
}
}, [accessToken, token, loading, message, navigate, profile]);

const text =
message ??
(accessToken
? 'Connecting To Discord...'
: 'Finishing Authentication Flow...');

return (
<div className="h-screen flex items-center justify-center flex-col gap-2">
<h1 className="text-lg">{text}</h1>
{loading && <Spinner />}
</div>
);
}
20 changes: 17 additions & 3 deletions frontend/src/pages/UserUpdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export function UserUpdate() {
const minecraftContext = useMinecraftContext(
profile?.connections?.minecraft_uuid,
);

const { beginFlow: beginMsFlow } = useOIDCProvider(MicrosoftProvider);

const user = useLoaderData() as User;
const navigate = useNavigate();

Expand All @@ -78,6 +80,7 @@ export function UserUpdate() {

useEffect(() => {
reloadAuthState();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
Expand Down Expand Up @@ -115,6 +118,11 @@ export function UserUpdate() {
[token, navigate, user.username],
);

const startDiscordFlow = () => {
const redirectUrl = encodeURI(`${window.location.protocol}//${window.location.host}/discord`);
window.location.href = `https://discord.com/oauth2/authorize?response_type=token&client_id=1247392795224838255&redirect_uri=${redirectUrl}&scope=identify`;
};

const userForm = useMemo(
() => (
<>
Expand Down Expand Up @@ -175,15 +183,21 @@ export function UserUpdate() {
<hr />
<h1 className="text-2xl">Connections</h1>
<ConnectionCard
title={`Minecraft${
minecraftSubtitle ? ` - ${minecraftSubtitle}` : ''
}`}
title={`Minecraft${minecraftSubtitle ? ` - ${minecraftSubtitle}` : ''
}`}
description="Connect your Microsoft account to enlist your Minecraft user for future sevres."
avatarImage={minecraftContext?.avatar}
connected={profile?.connections?.minecraft_uuid !== null}
onConnect={() => beginMsFlow()}
onDisconnect={() => alert('Unimplemented')}
/>
<ConnectionCard
title="Discord"
description="Connect your Discord account to be granted permissions in Realliance servers."
connected={profile?.connections?.discordId !== null}
onConnect={() => startDiscordFlow()}
onDisconnect={() => alert('Unimplemented')}
/>
</>
) : (
<AccessDenied />
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type Group = components['schemas']['Group'];
export type User = components['schemas']['User'];
export type NewGroup = components['schemas']['NewGroup'];
export type UpdateUser = components['schemas']['UpdateUser'];
export type MinecraftToken = components['schemas']['MinecraftToken'];
export type Token = components['schemas']['Token'];

const { GET, POST, PATCH } = createClient<paths>({ baseUrl: API_URL });

Expand Down Expand Up @@ -40,7 +40,7 @@ export const updateUser = (token: string, username: string, body: UpdateUser) =>
body,
});

export const addMinecraftToUser = (token: string, body: MinecraftToken) =>
export const addMinecraftToUser = (token: string, body: Token) =>
POST('/api/connections/minecraft', {
headers: {
Authorization: `Bearer ${token}`,
Expand Down Expand Up @@ -106,3 +106,11 @@ export const leaveGroup = (token: string, id: string) =>
},
},
});

export const addDiscordToUser = (token: string, body: Token) =>
POST('/api/connections/discord', {
headers: {
Authorization: `Bearer ${token}`,
},
body,
});
2 changes: 1 addition & 1 deletion frontend/src/util/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const useOIDCProvider = ({
new URL(expectedIssuer ?? issuer),
response,
),
), []);
), [expectedIssuer, issuer]);

return {
beginFlow: async () =>
Expand Down
29 changes: 27 additions & 2 deletions frontend/src/util/v1.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface paths {
post: operations["ConnectionController_addMinecraft"];
delete: operations["ConnectionController_removeMinecraft"];
};
"/api/connections/discord": {
post: operations["ConnectionController_addDiscord"];
delete: operations["ConnectionController_removeDiscord"];
};
"/api/groups/{id}": {
get: operations["GroupController_getById"];
};
Expand All @@ -44,6 +48,7 @@ export interface components {
Connection: {
userId: string;
minecraft_uuid?: string;
discordId?: string;
};
User: {
id: string;
Expand All @@ -64,7 +69,7 @@ export interface components {
description?: string;
pronouns?: string;
};
MinecraftToken: {
Token: {
token: string;
};
NewGroup: {
Expand Down Expand Up @@ -140,7 +145,7 @@ export interface operations {
/** @description payload containing the token */
requestBody: {
content: {
"application/json": components["schemas"]["MinecraftToken"];
"application/json": components["schemas"]["Token"];
};
};
responses: {
Expand All @@ -156,6 +161,26 @@ export interface operations {
};
};
};
ConnectionController_addDiscord: {
/** @description payload containing the token */
requestBody: {
content: {
"application/json": components["schemas"]["Token"];
};
};
responses: {
201: {
content: never;
};
};
};
ConnectionController_removeDiscord: {
responses: {
200: {
content: never;
};
};
};
GroupController_getById: {
parameters: {
path: {
Expand Down
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"incremental": true,
"skipLibCheck": true
}
}
Loading

0 comments on commit 18f097b

Please sign in to comment.