Skip to content

Commit

Permalink
🎉 feat: Discord roles integration (#373)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Feb 7, 2022
1 parent 4c38685 commit 136ed6c
Show file tree
Hide file tree
Showing 18 changed files with 635 additions and 28 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"tsconfig-paths": "^3.12.0"
},
"dependencies": {
"@discordjs/rest": "^0.3.0",
"@prisma/client": "^3.9.1",
"@snailycad/config": "1.0.0-beta.51",
"@snailycad/schemas": "1.0.0-beta.51",
Expand Down
48 changes: 48 additions & 0 deletions packages/api/prisma/migrations/20220207174037_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- AlterTable
ALTER TABLE "cad" ADD COLUMN "discordRolesId" TEXT;

-- CreateTable
CREATE TABLE "DiscordRoles" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"leoRoleId" TEXT,
"leoSupervisorRoleId" TEXT,
"emsFdRoleId" TEXT,
"dispatchRoleId" TEXT,
"towRoleId" TEXT,

CONSTRAINT "DiscordRoles_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "DiscordRole" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"discordRolesId" TEXT NOT NULL,

CONSTRAINT "DiscordRole_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "DiscordRole_id_key" ON "DiscordRole"("id");

-- AddForeignKey
ALTER TABLE "cad" ADD CONSTRAINT "cad_discordRolesId_fkey" FOREIGN KEY ("discordRolesId") REFERENCES "DiscordRoles"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRoles" ADD CONSTRAINT "DiscordRoles_leoRoleId_fkey" FOREIGN KEY ("leoRoleId") REFERENCES "DiscordRole"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRoles" ADD CONSTRAINT "DiscordRoles_leoSupervisorRoleId_fkey" FOREIGN KEY ("leoSupervisorRoleId") REFERENCES "DiscordRole"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRoles" ADD CONSTRAINT "DiscordRoles_emsFdRoleId_fkey" FOREIGN KEY ("emsFdRoleId") REFERENCES "DiscordRole"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRoles" ADD CONSTRAINT "DiscordRoles_dispatchRoleId_fkey" FOREIGN KEY ("dispatchRoleId") REFERENCES "DiscordRole"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRoles" ADD CONSTRAINT "DiscordRoles_towRoleId_fkey" FOREIGN KEY ("towRoleId") REFERENCES "DiscordRole"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DiscordRole" ADD CONSTRAINT "DiscordRole_discordRolesId_fkey" FOREIGN KEY ("discordRolesId") REFERENCES "DiscordRoles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
34 changes: 34 additions & 0 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ model cad {
updatedAt DateTime @default(now()) @updatedAt
autoSetUserProperties AutoSetUserProperties? @relation(fields: [autoSetUserPropertiesId], references: [id])
autoSetUserPropertiesId String?
discordRoles DiscordRoles? @relation(fields: [discordRolesId], references: [id])
discordRolesId String?
}

model MiscCadSettings {
Expand All @@ -55,6 +57,7 @@ model MiscCadSettings {
authScreenBgImageId String?
authScreenHeaderImageId String?
cad cad[]
}

Expand All @@ -79,6 +82,37 @@ model ApiToken {
cad cad[]
}

model DiscordRoles {
id String @id @default(cuid())
guildId String
leoRole DiscordRole? @relation("leoRole", fields: [leoRoleId], references: [id])
leoRoleId String?
leoSupervisorRole DiscordRole? @relation("leoSupervisorRole", fields: [leoSupervisorRoleId], references: [id])
leoSupervisorRoleId String?
emsFdRole DiscordRole? @relation("emsFdRole", fields: [emsFdRoleId], references: [id])
emsFdRoleId String?
dispatchRole DiscordRole? @relation("dispatchRole", fields: [dispatchRoleId], references: [id])
dispatchRoleId String?
towRole DiscordRole? @relation("towRole", fields: [towRoleId], references: [id])
towRoleId String?
roles DiscordRole[]
cad cad[]
}

model DiscordRole {
id String @id @unique
name String
discordRoles DiscordRoles @relation(fields: [discordRolesId], references: [id])
discordRolesId String
leoRoles DiscordRoles[] @relation("leoRole")
emsFdRoles DiscordRoles[] @relation("emsFdRole")
dispatchRoles DiscordRoles[] @relation("dispatchRole")
towRoles DiscordRoles[] @relation("towRole")
leoSupervisorRoles DiscordRoles[] @relation("leoSupervisorRole")
}

model User {
id String @id @default(cuid())
username String @unique @db.VarChar(255)
Expand Down
17 changes: 16 additions & 1 deletion packages/api/src/controllers/admin/manage/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { genSaltSync, hashSync } from "bcrypt";
import { citizenInclude } from "controllers/citizen/CitizenController";
import { validateSchema } from "lib/validateSchema";
import { ExtendedBadRequest } from "src/exceptions/ExtendedBadRequest";
import { updateMemberRoles } from "lib/discord/admin";
import { isDiscordIdInUse } from "utils/discord";

@UseBeforeEach(IsAuth)
@Controller("/admin/manage/users")
Expand Down Expand Up @@ -56,7 +58,11 @@ export class ManageUsersController {
}

@Put("/:id")
async updateUserById(@PathParams("id") userId: string, @BodyParams() body: unknown) {
async updateUserById(
@Context("cad") cad: { discordRolesId: string | null },
@PathParams("id") userId: string,
@BodyParams() body: unknown,
) {
const data = validateSchema(UPDATE_USER_SCHEMA, body);
const user = await prisma.user.findUnique({ where: { id: userId } });

Expand All @@ -68,6 +74,10 @@ export class ManageUsersController {
throw new ExtendedBadRequest({ rank: "cannotUpdateOwnerRank" });
}

if (data.discordId && (await isDiscordIdInUse(data.discordId, user.id))) {
throw new ExtendedBadRequest({ discordId: "discordIdInUse" });
}

const updated = await prisma.user.update({
where: {
id: user.id,
Expand All @@ -80,10 +90,15 @@ export class ManageUsersController {
isTow: data.isTow,
steamId: data.steamId,
rank: user.rank === Rank.OWNER ? Rank.OWNER : Rank[data.rank as Rank],
discordId: data.discordId,
},
select: userProperties,
});

if (updated.discordId) {
await updateMemberRoles(updated, cad.discordRolesId);
}

return updated;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import process from "node:process";
import { BodyParams, Context, Controller, UseBeforeEach } from "@tsed/common";
import { Get, Post } from "@tsed/schema";
import { RESTGetAPIGuildRolesResult, Routes } from "discord-api-types/v9";
import { IsAuth } from "middlewares/IsAuth";
import { prisma } from "lib/prisma";
import type { cad } from "@prisma/client";
import { BadRequest } from "@tsed/exceptions";
import { DISCORD_SETTINGS_SCHEMA } from "@snailycad/schemas";
import { validateSchema } from "lib/validateSchema";
import { getRest } from "lib/discord";

const guildId = process.env.DISCORD_SERVER_ID;

@Controller("/admin/manage/cad-settings/discord")
@UseBeforeEach(IsAuth)
export class DiscordSettingsController {
@Get("/")
async getGuildRoles(@Context("cad") cad: cad) {
if (!guildId) {
throw new BadRequest("mustSetBotTokenGuildId");
}

const rest = getRest();
const roles = (await rest.get(
`${Routes.guildRoles(guildId)}`,
)) as RESTGetAPIGuildRolesResult | null;

const discordRoles = await prisma.discordRoles.upsert({
where: { id: String(cad.discordRolesId) },
update: { guildId },
create: {
guildId,
},
});

await prisma.cad.update({
where: { id: cad.id },
data: { discordRolesId: discordRoles.id },
});

const rolesBody = Array.isArray(roles) ? roles : [];
const data = [];

for (const role of rolesBody) {
if (role.name === "@everyone") continue;

const discordRole = await prisma.discordRole.upsert({
where: { id: role.id },
create: {
name: role.name,
id: role.id,
discordRolesId: discordRoles.id,
},
update: {
name: role.name,
discordRolesId: discordRoles.id,
},
});

data.push(discordRole);
}

return data;
}

@Post("/")
async setRoleTypes(@Context("cad") cad: cad, @BodyParams() body: unknown) {
if (!guildId) {
throw new BadRequest("mustSetBotTokenGuildId");
}

const data = validateSchema(DISCORD_SETTINGS_SCHEMA, body);

const rest = getRest();
const roles = (await rest.get(
`${Routes.guildRoles(guildId)}`,
)) as RESTGetAPIGuildRolesResult | null;

const rolesBody = Array.isArray(roles) ? roles : [];

Object.values(data).map((roleId) => {
if (roleId && !this.doesRoleExist(rolesBody, roleId)) {
throw new BadRequest("invalidRoleId");
}
});

const createUpdateData = {
guildId,
dispatchRoleId: data.dispatchRoleId ?? null,
leoRoleId: data.leoRoleId ?? null,
leoSupervisorRoleId: data.leoSupervisorRoleId ?? null,
emsFdRoleId: data.emsFdRoleId ?? null,
towRoleId: data.towRoleId ?? null,
};

const discordRoles = await prisma.discordRoles.upsert({
where: { id: String(cad.discordRolesId) },
update: createUpdateData,
create: createUpdateData,
});

await prisma.cad.update({
where: { id: cad.id },
data: { discordRolesId: discordRoles.id },
});

return discordRoles;
}

protected doesRoleExist(roles: { id: string }[], roleId: string) {
return roles.some((v) => v.id === roleId);
}
}
22 changes: 14 additions & 8 deletions packages/api/src/controllers/auth/Discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { signJWT } from "utils/jwt";
import { setCookie } from "utils/setCookie";
import { Cookie } from "@snailycad/config";
import { IsAuth } from "middlewares/index";
import { DISCORD_API_URL } from "lib/discord";
import { updateMemberRolesLogin } from "lib/discord/auth";

const DISCORD_API_VERSION = "v9";
const discordApiUrl = `https://discord.com/api/${DISCORD_API_VERSION}`;
const callbackUrl = makeCallbackURL(findUrl());
const DISCORD_CLIENT_ID = process.env["DISCORD_CLIENT_ID"];
const DISCORD_CLIENT_SECRET = process.env["DISCORD_CLIENT_SECRET"];
Expand All @@ -25,7 +25,7 @@ const DISCORD_CLIENT_SECRET = process.env["DISCORD_CLIENT_SECRET"];
export class DiscordAuth {
@Get("/")
async handleRedirectToDiscordOAuthAPI(@Res() res: Res) {
const url = new URL(`${discordApiUrl}/oauth2/authorize`);
const url = new URL(`${DISCORD_API_URL}/oauth2/authorize`);

if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET) {
throw new BadRequest(
Expand Down Expand Up @@ -70,13 +70,17 @@ export class DiscordAuth {
where: { discordId: data.id },
});

const cad = await prisma.cad.findFirst();
const discordRolesId = cad?.discordRolesId ?? null;

/**
* a user was found with the discordId, but the user is not authenticated.
*
* -> log the user in and set the cookie
*/
if (!authUser && user) {
validateUser(user);
await updateMemberRolesLogin(user, discordRolesId);

// authenticate user with cookie
const jwtToken = signJWT({ userId: user.id }, AUTH_TOKEN_EXPIRES_S);
Expand Down Expand Up @@ -120,12 +124,13 @@ export class DiscordAuth {
value: jwtToken,
});

await updateMemberRolesLogin(user, discordRolesId);
return res.redirect(`${redirectURL}/citizen`);
}

if (authUser && user) {
if (user.id === authUser.id) {
await prisma.user.update({
const updated = await prisma.user.update({
where: {
id: authUser.id,
},
Expand All @@ -135,6 +140,7 @@ export class DiscordAuth {
});

validateUser(user);
await updateMemberRolesLogin(updated, discordRolesId);

return res.redirect(`${redirectURL}/account?tab=discord&success`);
}
Expand All @@ -143,7 +149,7 @@ export class DiscordAuth {
}

if (authUser && !user) {
await prisma.user.update({
const updated = await prisma.user.update({
where: {
id: authUser.id,
},
Expand All @@ -153,6 +159,7 @@ export class DiscordAuth {
});

validateUser(authUser);
await updateMemberRolesLogin(updated, discordRolesId);

return res.redirect(`${redirectURL}/account?tab=discord&success`);
}
Expand Down Expand Up @@ -190,7 +197,7 @@ export class DiscordAuth {

async function getDiscordData(code: string): Promise<APIUser | null> {
const data = (await request(
`${discordApiUrl}/oauth2/token?grant_type=authorization_code&code=${code}&redirect_uri=${callbackUrl}`,
`${DISCORD_API_URL}/oauth2/token?grant_type=authorization_code&code=${code}&redirect_uri=${callbackUrl}`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
Expand All @@ -206,8 +213,7 @@ async function getDiscordData(code: string): Promise<APIUser | null> {
).then((v) => v.body.json())) as RESTPostOAuth2AccessTokenResult;

const accessToken = data.access_token;

const meData = await request(`${discordApiUrl}/users/@me`, {
const meData = await request(`${DISCORD_API_URL}/users/@me`, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
Expand Down
Loading

0 comments on commit 136ed6c

Please sign in to comment.