Skip to content

Commit

Permalink
Merge branch 'main' into frontpage-rework
Browse files Browse the repository at this point in the history
  • Loading branch information
JJ-8 committed Aug 25, 2024
2 parents 49f20a1 + 4045522 commit 995b14b
Show file tree
Hide file tree
Showing 116 changed files with 1,047 additions and 359 deletions.
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
# DISCORD_VOICE_CHANNELS=3
# DISCORD_BOT_NAME=CTFNote

# Enable this if you want users to be able to make accounts through the /register command in discord
# DISCORD_REGISTRATION_ENABLED=false

# Which role the user should be granted on the ctfnote when creating a account through the bot
# DISCORD_REGISTRATION_CTFNOTE_ROLE=user_guest

# If you want the bot to verify if a user has a specific role in the discord before allowing them to make a account through
# the Discord command, set the ID of the role below, else leave this field empty.
#DISCORD_REGISTRATION_ROLE_ID=discord_id

# Configure timezone and locale
# TZ=Europe/Paris
# LC_ALL=en_US.UTF-8
Expand Down
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/.yarn/releases/** binary
**/.yarn/plugins/** binary
11 changes: 3 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,11 @@ temp/
# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored

.yarn/*
!.yarn/releases
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# if you are NOT using Zero-installs, then:
# comment the following lines
!.yarn/cache

# and uncomment the following lines
# .pnp.*

# End of https://www.toptal.com/developers/gitignore/api/yarn,node
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"search.exclude": {
"**/.yarn": true,
"**/node_modules": true
}
}
11 changes: 3 additions & 8 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,13 @@ temp/
# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored

.yarn/*
!.yarn/releases
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# if you are NOT using Zero-installs, then:
# comment the following lines
!.yarn/cache

# and uncomment the following lines
# .pnp.*

# End of https://www.toptal.com/developers/gitignore/api/yarn,node


Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
104 changes: 104 additions & 0 deletions api/migrations/55-discord-account-invitation-link.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
ALTER TABLE ctfnote_private.invitation_link
ADD COLUMN "discord_id" TEXT UNIQUE DEFAULT NULL;

DROP FUNCTION ctfnote.create_invitation_link ("role" ctfnote.role);
CREATE OR REPLACE FUNCTION ctfnote.create_invitation_link ("role" ctfnote.role, "discord_id" text default null)
RETURNS ctfnote.invitation_link_response
AS $$
DECLARE
invitation_link ctfnote_private.invitation_link;
BEGIN
INSERT INTO ctfnote_private.invitation_link ("role", "token", "discord_id")
VALUES (create_invitation_link.role, gen_random_uuid (), create_invitation_link.discord_id)
RETURNING
* INTO invitation_link;
RETURN ROW (invitation_link.token::text)::ctfnote.invitation_link_response;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION ctfnote.create_invitation_link (ctfnote.role, text) TO user_admin;

CREATE OR REPLACE FUNCTION ctfnote.register_with_token ("token" text, "login" text, "password" text)
RETURNS ctfnote.jwt
AS $$
DECLARE
invitation_role ctfnote.role;
invitation_discord_id text;
BEGIN
SELECT
ROLE, discord_id INTO invitation_role, invitation_discord_id
FROM
ctfnote_private.invitation_link
WHERE
invitation_link.token::text = register_with_token.token
AND expiration > now();
IF invitation_role IS NOT NULL THEN
DELETE FROM ctfnote_private.invitation_link
WHERE invitation_link.token::text = register_with_token.token;
IF invitation_discord_id IS NOT NULL THEN
RETURN ctfnote_private.do_register (register_with_token.login, register_with_token.password, invitation_role, invitation_discord_id);
ELSE
RETURN ctfnote_private.do_register (register_with_token.login, register_with_token.password, invitation_role);
END IF;
ELSE
RAISE EXCEPTION 'Invalid token';
END IF;
END
$$
LANGUAGE plpgsql
SECURITY DEFINER;

GRANT EXECUTE ON FUNCTION ctfnote.register_with_token (text, text, text) TO user_anonymous;

-- first we remove and re-apply the old internal registration function to be extra verbose
-- we implement the additional logic for registration with discord_id in a seperate function with the same name, thus overloading this function for normal original operation and
-- operation with the new discord id linking.
DROP FUNCTION ctfnote_private.do_register ("login" text, "password" text, "role" ctfnote.role);

CREATE OR REPLACE FUNCTION ctfnote_private.do_register ("login" text, "password" text, "role" ctfnote.role)
RETURNS ctfnote.jwt
AS $$
DECLARE
new_user ctfnote_private.user;
BEGIN
INSERT INTO ctfnote_private.user ("login", "password", "role")
VALUES (do_register.login, crypt(do_register.password, gen_salt('bf')), do_register.role)
RETURNING
* INTO new_user;
INSERT INTO ctfnote.profile ("id", "username")
VALUES (new_user.id, do_register.login);
RETURN (ctfnote_private.new_token (new_user.id))::ctfnote.jwt;
EXCEPTION
WHEN unique_violation THEN
RAISE EXCEPTION 'Username already taken';
END;
$$
LANGUAGE plpgsql
STRICT
SECURITY DEFINER;

-- overloaded function, implements the logic needed for discord linking.
CREATE OR REPLACE FUNCTION ctfnote_private.do_register ("login" text, "password" text, "role" ctfnote.role, "discord_id" text)
RETURNS ctfnote.jwt
AS $$
DECLARE
new_user ctfnote_private.user;
BEGIN
INSERT INTO ctfnote_private.user ("login", "password", "role")
VALUES (do_register.login, crypt(do_register.password, gen_salt('bf')), do_register.role)
RETURNING
* INTO new_user;
INSERT INTO ctfnote.profile ("id", "username", "discord_id")
VALUES (new_user.id, do_register.login, do_register.discord_id);
RETURN (ctfnote_private.new_token (new_user.id))::ctfnote.jwt;
EXCEPTION
WHEN unique_violation THEN
RAISE EXCEPTION 'Username already taken';
END;
$$
LANGUAGE plpgsql
STRICT
SECURITY DEFINER;

8 changes: 8 additions & 0 deletions api/migrations/56-add-discord-integration-enabled.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE ctfnote.settings
ADD COLUMN "discord_integration_enabled" boolean NOT NULL DEFAULT FALSE;

GRANT SELECT ("discord_integration_enabled") ON ctfnote.settings TO user_anonymous;
REVOKE UPDATE ON ctfnote.settings FROM user_admin;
GRANT UPDATE (unique_id, registration_allowed, registration_password_allowed, registration_password, registration_default_role, style, ical_password) ON ctfnote.settings TO user_admin;
GRANT UPDATE ("discord_integration_enabled") ON ctfnote.settings TO user_postgraphile;

6 changes: 3 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"axios": "^1.6.8",
"discord.js": "^14.14.1",
"dotenv": "^16.4.5",
"express": "^4.19.1",
"express": "^4.19.2",
"graphile-utils": "4.13.0",
"graphql": "^16.8.1",
"graphql-upload-ts": "^2.1.2",
Expand All @@ -38,13 +38,13 @@
"devDependencies": {
"@types/express": "^4.17.21",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@typescript-eslint/parser": "^7.16.0",
"eslint": "^8.57.0",
"lint-staged": "^15.2.2",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
"typescript": "^5.4.5"
},
"packageManager": "[email protected]"
}
11 changes: 10 additions & 1 deletion api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export type CTFNoteConfig = DeepReadOnly<{
voiceChannels: number;
botName: string;
maxChannelsPerCategory: number;
registrationEnabled: string;
registrationAccountRole: string;
registrationRoleId: string;
};
}>;

Expand Down Expand Up @@ -92,7 +95,13 @@ const config: CTFNoteConfig = {
serverId: getEnv("DISCORD_SERVER_ID"),
voiceChannels: getEnvInt("DISCORD_VOICE_CHANNELS"),
botName: getEnv("DISCORD_BOT_NAME", "CTFNote"),
maxChannelsPerCategory: 50, // 50 is the hard Discord limit
maxChannelsPerCategory: 50, //! 50 is the hard Discord limit
registrationEnabled: getEnv("DISCORD_REGISTRATION_ENABLED", "false"),
registrationAccountRole: getEnv(
"DISCORD_REGISTRATION_CTFNOTE_ROLE",
"user_guest"
),
registrationRoleId: getEnv("DISCORD_REGISTRATION_ROLE_ID", ""),
},
};

Expand Down
2 changes: 2 additions & 0 deletions api/src/discord/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SolveTask } from "./commands/solveTask";
import { LinkUser } from "./commands/linkUser";
import { StartWorking, StopWorking } from "./commands/workingOn";
import { DeleteCtf } from "./commands/deleteCtf";
import { Register } from "./commands/register";

export const Commands: Command[] = [
ArchiveCtf,
Expand All @@ -14,4 +15,5 @@ export const Commands: Command[] = [
StartWorking,
StopWorking,
DeleteCtf,
Register,
];
127 changes: 127 additions & 0 deletions api/src/discord/commands/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
ApplicationCommandType,
Client,
CommandInteraction,
GuildMemberRoleManager,
} from "discord.js";
import { Command } from "../command";
import {
AllowedRoles,
createInvitationTokenForDiscordId,
getInvitationTokenForDiscordId,
getUserByDiscordId,
} from "../database/users";
import config from "../../config";

async function getInvitationUrl(invitationCode: string | null = null) {
if (config.pad.domain == "") return null;
if (invitationCode == null) return null;

const ssl = config.pad.useSSL == "false" ? "" : "s";

return `http${ssl}://${config.pad.domain}/#/auth/register/${invitationCode}`;
}

async function getProfileUrl() {
if (config.pad.domain == "") return null;

const ssl = config.pad.useSSL == "false" ? "" : "s";

return `http${ssl}://${config.pad.domain}/#/user/settings`;
}

async function registerLogic(client: Client, interaction: CommandInteraction) {
if (config.discord.registrationEnabled.toLowerCase() !== "true") {
await interaction.editReply({
content:
"The functionality to create your own account this way has been disabled by an administrator.",
});
return;
}

if (config.discord.registrationRoleId !== "") {
if (
!(interaction.member?.roles as GuildMemberRoleManager).cache.has(
config.discord.registrationRoleId
)
) {
await interaction.editReply({
content:
"You do not have the role required to create an account yourself.",
});
return;
}
}

const userId = await getUserByDiscordId(interaction.user.id);
if (userId != null) {
await interaction.editReply({
content:
"You can't link the same Discord account twice! If you do not have a CTFNote account or haven't linked it, contact an administrator.",
});
return;
}

const existingInvitationCode = await getInvitationTokenForDiscordId(
interaction.user.id
);
if (existingInvitationCode != null) {
const invitationUrl = await getInvitationUrl(existingInvitationCode);
if (invitationUrl == null) {
await interaction.editReply({
content:
"Could not generate invitation URL. Please contact an administrator.",
});
return;
}

await interaction.editReply({
content: `Your personal invitation url: ${invitationUrl}.\n-# If you already have a CTFNote account you should link it using the \`/link\` command using the Discord token from your profile: ${await getProfileUrl()}.`,
});
return;
}

await interaction.editReply({
content:
"Generating a private invitation URL... If you already have a CTFNote account you should link it using the `/link` command instead.",
});

const invitationCode = await createInvitationTokenForDiscordId(
interaction.user.id,
(config.discord.registrationAccountRole as AllowedRoles) ??
AllowedRoles.user_guest
);

if (invitationCode == null) {
await interaction.editReply({
content:
"Could not generate an invitation code. Please contact an administrator.",
});
return;
}

const invitationUrl = await getInvitationUrl(invitationCode);
if (invitationUrl == null) {
await interaction.editReply({
content:
"Could not get an invitation URL. Please contact an administrator.",
});
return;
}

await interaction.editReply({
content: `Your personal invitation url: ${invitationUrl}.\n-# If you already have a CTFNote account you should link it using the \`/link\` command using the Discord token from your profile: ${await getProfileUrl()}.`,
});
return;
}

export const Register: Command = {
name: "register",
description: "Create an account on CTFNote (if enabled)!",
type: ApplicationCommandType.ChatInput,
run: async (client, interaction) => {
return registerLogic(client, interaction).catch((e) => {
console.error("Error during /register Discord logic: ", e);
});
},
};
Loading

0 comments on commit 995b14b

Please sign in to comment.