diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f18e7c6ad --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,107 @@ +name: Continuous Integration + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:latest + env: + POSTGRES_PASSWORD: ctfnote + POSTGRES_USER: ctfnote + ports: + - 5432:5432 + + strategy: + fail-fast: false + matrix: + node: [20, 19, 18] + + name: Node.js ${{ matrix.node }} build + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: "yarn" + cache-dependency-path: | + api/yarn.lock + front/yarn.lock + + - name: Build api + working-directory: ./api + run: | + yarn install --immutable --immutable-cache --check-cache + yarn build + yarn run pg-validate-migrations ./migrations + + - name: Build frontend + working-directory: ./front + run: | + yarn install --immutable --immutable-cache --check-cache + yarn build + + - name: Run database migrations and codegen + env: + PAD_CREATE_URL: http://hedgedoc:3000/new + PAD_SHOW_URL: / + DB_DATABASE: ctfnote + DB_ADMIN_LOGIN: ctfnote + DB_ADMIN_PASSWORD: ctfnote + DB_USER_LOGIN: user_postgraphile + DB_USER_PASSWORD: secret_password + DB_HOST: 127.0.0.1 + DB_PORT: 5432 + WEB_PORT: 3000 + run: | + cd ./api + # create database tables first, running migrations + DB_MIGRATE_ONLY=1 yarn start + # then start the api backend server + nohup yarn start & + cd ../front + # and finally validate the generated files are up to date + bash ../api/start.sh 127.0.0.1 3000 yarn run graphql-codegen --config codegen.yml --check + + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "yarn" + cache-dependency-path: | + api/yarn.lock + front/yarn.lock + + - name: Install frontend dependencies + working-directory: ./front + run: yarn install --immutable --immutable-cache --check-cache + + - name: Lint frontend + working-directory: ./front + run: | + yarn run eslint --ext .js,.ts,.vue,.graphql ./src + yarn run prettier --check 'src/**/*.{ts,js,vue,graphql}' + + - name: Install api dependencies + working-directory: ./api + run: yarn install --immutable --immutable-cache --check-cache + + - name: Lint api + working-directory: ./api + run: | + yarn run eslint --ext .ts ./src + yarn run prettier --check 'src/**/*.ts' diff --git a/api/src/config.ts b/api/src/config.ts index d626de48f..4076db417 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -22,6 +22,7 @@ export type CTFNoteConfig = DeepReadOnly<{ }; host: string; port: number; + migrateOnly: boolean; }; pad: { createUrl: string; @@ -73,6 +74,7 @@ const config: CTFNoteConfig = { }, host: getEnv("DB_HOST"), port: getEnvInt("DB_PORT"), + migrateOnly: !!process.env["DB_MIGRATE_ONLY"], }, pad: { createUrl: getEnv("PAD_CREATE_URL"), diff --git a/api/src/discord/commands/archiveCtf.ts b/api/src/discord/commands/archiveCtf.ts index d2a7c66af..6ee092689 100644 --- a/api/src/discord/commands/archiveCtf.ts +++ b/api/src/discord/commands/archiveCtf.ts @@ -82,7 +82,9 @@ async function archiveCtfLogic( ); } - const actionRow: any = new ActionRowBuilder().addComponents(buttons); + const actionRow = new ActionRowBuilder().addComponents( + buttons + ); await interaction.editReply({ content: "Which CTF do you want to archive?", diff --git a/api/src/discord/commands/createCtf.ts b/api/src/discord/commands/createCtf.ts index 9487d1b4f..706ccfe57 100644 --- a/api/src/discord/commands/createCtf.ts +++ b/api/src/discord/commands/createCtf.ts @@ -48,7 +48,9 @@ async function createCtfLogic(client: Client, interaction: CommandInteraction) { } // Create the action row with the button components - const actionRow: any = new ActionRowBuilder().addComponents(buttons); + const actionRow = new ActionRowBuilder().addComponents( + buttons + ); await interaction.editReply({ content: ctfNamesMessage, diff --git a/api/src/discord/commands/deleteCtf.ts b/api/src/discord/commands/deleteCtf.ts index 2cce35c63..226719284 100644 --- a/api/src/discord/commands/deleteCtf.ts +++ b/api/src/discord/commands/deleteCtf.ts @@ -3,7 +3,6 @@ import { ApplicationCommandType, ButtonBuilder, ButtonStyle, - ChannelType, Client, CommandInteraction, Interaction, @@ -64,7 +63,9 @@ async function deleteCtfLogic(client: Client, interaction: CommandInteraction) { ); } - const actionRow: any = new ActionRowBuilder().addComponents(buttons); + const actionRow = new ActionRowBuilder().addComponents( + buttons + ); await interaction.editReply({ content: diff --git a/api/src/discord/database/tasks.ts b/api/src/discord/database/tasks.ts index 03c7997ae..fb03ee168 100644 --- a/api/src/discord/database/tasks.ts +++ b/api/src/discord/database/tasks.ts @@ -10,6 +10,7 @@ export interface Task { flag: string; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function buildTask(row: any): Task { return { id: row.id as bigint, diff --git a/api/src/index.ts b/api/src/index.ts index f9f7060fa..8d92cc41e 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -145,6 +145,10 @@ async function performMigrations() { async function main() { await performMigrations(); + if (config.db.migrateOnly) { + console.log("Migrations done. Exiting."); + return; + } const postgraphileOptions = createOptions(); const app = createApp(postgraphileOptions); diff --git a/api/src/plugins/savepointWrapper.ts b/api/src/plugins/savepointWrapper.ts index 518d914c3..da71b3d3a 100644 --- a/api/src/plugins/savepointWrapper.ts +++ b/api/src/plugins/savepointWrapper.ts @@ -1,6 +1,9 @@ import { Client } from "pg"; -async function savepointWrapper(pgClient: Client, f: () => any): Promise { +async function savepointWrapper( + pgClient: Client, + f: () => Type +): Promise { const name = `"CHECKPOINT-${Math.floor(Math.random() * 0xffff)}"`; await pgClient.query(`SAVEPOINT ${name}`); try { diff --git a/front/graphql.schema.json b/front/graphql.schema.json index 15f5b7ba2..9f91ea6af 100644 --- a/front/graphql.schema.json +++ b/front/graphql.schema.json @@ -220,6 +220,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "description": "A filter to be used against `AssignedTag` object types. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "and", + "description": "Checks for all expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not", + "description": "Negates the expression.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "or", + "description": "Checks for any expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tagId", + "description": "Filter by the object’s `tagId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taskId", + "description": "Filter by the object’s `taskId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "AssignedTagInput", @@ -456,6 +543,165 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "BooleanFilter", + "description": "A filter to be used against Boolean fields. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "distinctFrom", + "description": "Not equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "equalTo", + "description": "Equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThan", + "description": "Greater than the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Greater than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "in", + "description": "Included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isNull", + "description": "Is null (if `true` is specified) or is not null (if `false` is specified).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThan", + "description": "Less than the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Less than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFrom", + "description": "Equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualTo", + "description": "Not equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIn", + "description": "Not included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CancelWorkingOnInput", @@ -1822,6 +2068,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -2346,6 +2604,42 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "endTime", + "description": "Filter by the object’s `endTime` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "DatetimeFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "granted", + "description": "Filter by the object’s `granted` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "BooleanFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Filter by the object’s `id` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "not", "description": "Negates the expression.", @@ -2379,8 +2673,32 @@ "deprecationReason": null }, { - "name": "title", - "description": "Filter by the object’s `title` field.", + "name": "secretsId", + "description": "Filter by the object’s `secretsId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startTime", + "description": "Filter by the object’s `startTime` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "DatetimeFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "Filter by the object’s `title` field.", "type": { "kind": "INPUT_OBJECT", "name": "StringFilter", @@ -2968,6 +3286,81 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CtfSecretFilter", + "description": "A filter to be used against `CtfSecret` object types. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "and", + "description": "Checks for all expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CtfSecretFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Filter by the object’s `id` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not", + "description": "Negates the expression.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CtfSecretFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "or", + "description": "Checks for any expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CtfSecretFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CtfSecretPatch", @@ -3401,6 +3794,165 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "DatetimeFilter", + "description": "A filter to be used against Datetime fields. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "distinctFrom", + "description": "Not equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "equalTo", + "description": "Equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThan", + "description": "Greater than the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Greater than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "in", + "description": "Included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isNull", + "description": "Is null (if `true` is specified) or is not null (if `false` is specified).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThan", + "description": "Less than the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Less than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFrom", + "description": "Equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualTo", + "description": "Not equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIn", + "description": "Not included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Datetime", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "DeleteAssignedTagByNodeIdInput", @@ -4570,44 +5122,203 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "Invitation", - "description": null, - "fields": [ + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "description": "A filter to be used against Int fields. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ { - "name": "ctf", - "description": "Reads a single `Ctf` that is related to this `Invitation`.", - "args": [], + "name": "distinctFrom", + "description": "Not equal to the specified value, treating null like an ordinary value.", "type": { - "kind": "OBJECT", - "name": "Ctf", + "kind": "SCALAR", + "name": "Int", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "ctfId", - "description": null, - "args": [], + "name": "equalTo", + "description": "Equal to the specified value.", "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "nodeId", - "description": "A globally unique identifier. Can be used in various places throughout the system to identify this single value.", - "args": [], + "name": "greaterThan", + "description": "Greater than the specified value.", "type": { - "kind": "NON_NULL", + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Greater than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "in", + "description": "Included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isNull", + "description": "Is null (if `true` is specified) or is not null (if `false` is specified).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThan", + "description": "Less than the specified value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Less than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFrom", + "description": "Equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualTo", + "description": "Not equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIn", + "description": "Not included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Invitation", + "description": null, + "fields": [ + { + "name": "ctf", + "description": "Reads a single `Ctf` that is related to this `Invitation`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Ctf", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ctfId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodeId", + "description": "A globally unique identifier. Can be used in various places throughout the system to identify this single value.", + "args": [], + "type": { + "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", @@ -4693,6 +5404,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "description": "A filter to be used against `Invitation` object types. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "and", + "description": "Checks for all expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ctfId", + "description": "Filter by the object’s `ctfId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not", + "description": "Negates the expression.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "or", + "description": "Checks for any expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "profileId", + "description": "Filter by the object’s `profileId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "InvitationInput", @@ -6906,6 +7704,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -7196,6 +8006,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -7493,11 +8315,11 @@ "deprecationReason": null }, { - "name": "not", - "description": "Negates the expression.", + "name": "id", + "description": "Filter by the object’s `id` field.", "type": { "kind": "INPUT_OBJECT", - "name": "ProfileFilter", + "name": "IntFilter", "ofType": null }, "defaultValue": null, @@ -7505,8 +8327,20 @@ "deprecationReason": null }, { - "name": "or", - "description": "Checks for any expressions in this list.", + "name": "not", + "description": "Negates the expression.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ProfileFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "or", + "description": "Checks for any expressions in this list.", "type": { "kind": "LIST", "name": null, @@ -7524,6 +8358,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "role", + "description": "Filter by the object’s `role` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RoleFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "username", "description": "Filter by the object’s `username` field.", @@ -8313,6 +9159,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -8534,6 +9392,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CtfSecretFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -8744,6 +9614,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ProfileFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -8817,6 +9699,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CtfFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -8976,6 +9870,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InvitationFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -9138,6 +10044,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CtfFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -10245,6 +11163,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -10946,6 +11876,165 @@ ], "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "RoleFilter", + "description": "A filter to be used against Role fields. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "distinctFrom", + "description": "Not equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "equalTo", + "description": "Equal to the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThan", + "description": "Greater than the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Greater than or equal to the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "in", + "description": "Included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Role", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isNull", + "description": "Is null (if `true` is specified) or is not null (if `false` is specified).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThan", + "description": "Less than the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Less than or equal to the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFrom", + "description": "Equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualTo", + "description": "Not equal to the specified value.", + "type": { + "kind": "ENUM", + "name": "Role", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIn", + "description": "Not included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Role", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "SetDiscordEventLinkInput", @@ -11557,141 +12646,605 @@ "description": null, "type": { "kind": "SCALAR", - "name": "Int", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "StopWorkingOnPayload", + "description": "The output of our `stopWorkingOn` mutation.", + "fields": [ + { + "name": "clientMutationId", + "description": "The exact same `clientMutationId` that was provided in the mutation input,\nunchanged and unused. May be used by a client to track mutations.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "profile", + "description": "Reads a single `Profile` that is related to this `WorkOnTask`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Profile", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "Our root query field type. Allows us to run any query from our mutation payload.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Query", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "task", + "description": "Reads a single `Task` that is related to this `WorkOnTask`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Task", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workOnTask", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "WorkOnTask", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "workOnTaskEdge", + "description": "An edge for our `WorkOnTask`. May be used by Relay 1.", + "args": [ + { + "name": "orderBy", + "description": "The method to use when ordering `WorkOnTask`.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "WorkOnTasksOrderBy", + "ofType": null + } + } + }, + "defaultValue": "[PRIMARY_KEY_ASC]", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "WorkOnTasksEdge", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "StringFilter", + "description": "A filter to be used against String fields. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "distinctFrom", + "description": "Not equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "distinctFromInsensitive", + "description": "Not equal to the specified value, treating null like an ordinary value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endsWith", + "description": "Ends with the specified string (case-sensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endsWithInsensitive", + "description": "Ends with the specified string (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "equalTo", + "description": "Equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "equalToInsensitive", + "description": "Equal to the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThan", + "description": "Greater than the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanInsensitive", + "description": "Greater than the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Greater than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "greaterThanOrEqualToInsensitive", + "description": "Greater than or equal to the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "in", + "description": "Included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inInsensitive", + "description": "Included in the specified list (case-insensitive).", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includes", + "description": "Contains the specified string (case-sensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "includesInsensitive", + "description": "Contains the specified string (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isNull", + "description": "Is null (if `true` is specified) or is not null (if `false` is specified).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThan", + "description": "Less than the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanInsensitive", + "description": "Less than the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Less than or equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lessThanOrEqualToInsensitive", + "description": "Less than or equal to the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "like", + "description": "Matches the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "likeInsensitive", + "description": "Matches the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFrom", + "description": "Equal to the specified value, treating null like an ordinary value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notDistinctFromInsensitive", + "description": "Equal to the specified value, treating null like an ordinary value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEndsWith", + "description": "Does not end with the specified string (case-sensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEndsWithInsensitive", + "description": "Does not end with the specified string (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualTo", + "description": "Not equal to the specified value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notEqualToInsensitive", + "description": "Not equal to the specified value (case-insensitive).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIn", + "description": "Not included in the specified list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notInInsensitive", + "description": "Not included in the specified list (case-insensitive).", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notIncludes", + "description": "Does not contain the specified string (case-sensitive).", + "type": { + "kind": "SCALAR", + "name": "String", "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "StopWorkingOnPayload", - "description": "The output of our `stopWorkingOn` mutation.", - "fields": [ + }, { - "name": "clientMutationId", - "description": "The exact same `clientMutationId` that was provided in the mutation input,\nunchanged and unused. May be used by a client to track mutations.", - "args": [], + "name": "notIncludesInsensitive", + "description": "Does not contain the specified string (case-insensitive).", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "profile", - "description": "Reads a single `Profile` that is related to this `WorkOnTask`.", - "args": [], + "name": "notLike", + "description": "Does not match the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", "type": { - "kind": "OBJECT", - "name": "Profile", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "query", - "description": "Our root query field type. Allows us to run any query from our mutation payload.", - "args": [], + "name": "notLikeInsensitive", + "description": "Does not match the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", "type": { - "kind": "OBJECT", - "name": "Query", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "task", - "description": "Reads a single `Task` that is related to this `WorkOnTask`.", - "args": [], + "name": "notStartsWith", + "description": "Does not start with the specified string (case-sensitive).", "type": { - "kind": "OBJECT", - "name": "Task", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "workOnTask", - "description": null, - "args": [], + "name": "notStartsWithInsensitive", + "description": "Does not start with the specified string (case-insensitive).", "type": { - "kind": "OBJECT", - "name": "WorkOnTask", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "workOnTaskEdge", - "description": "An edge for our `WorkOnTask`. May be used by Relay 1.", - "args": [ - { - "name": "orderBy", - "description": "The method to use when ordering `WorkOnTask`.", - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "WorkOnTasksOrderBy", - "ofType": null - } - } - }, - "defaultValue": "[PRIMARY_KEY_ASC]", - "isDeprecated": false, - "deprecationReason": null - } - ], + "name": "startsWith", + "description": "Starts with the specified string (case-sensitive).", "type": { - "kind": "OBJECT", - "name": "WorkOnTasksEdge", + "kind": "SCALAR", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "String", - "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "StringFilter", - "description": "A filter to be used against String fields. All fields are combined with a logical ‘and.’", - "fields": null, - "inputFields": [ + }, { - "name": "includesInsensitive", - "description": "Contains the specified string (case-insensitive).", + "name": "startsWithInsensitive", + "description": "Starts with the specified string (case-insensitive).", "type": { "kind": "SCALAR", "name": "String", @@ -11831,6 +13384,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -12142,6 +13707,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "id", + "description": "Filter by the object’s `id` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "not", "description": "Negates the expression.", @@ -12588,6 +14165,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AssignedTagFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -13075,6 +14664,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": "A filter to be used in determining which values should be returned by the collection.", + "type": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": "Only read the first `n` values of the set.", @@ -13229,6 +14830,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "ctfId", + "description": "Filter by the object’s `ctfId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Filter by the object’s `id` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "not", "description": "Negates the expression.", @@ -13261,6 +14886,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "solved", + "description": "Filter by the object’s `solved` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "BooleanFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "title", "description": "Filter by the object’s `title` field.", @@ -15622,6 +17259,93 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "description": "A filter to be used against `WorkOnTask` object types. All fields are combined with a logical ‘and.’", + "fields": null, + "inputFields": [ + { + "name": "and", + "description": "Checks for all expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "not", + "description": "Negates the expression.", + "type": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "or", + "description": "Checks for any expressions in this list.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "WorkOnTaskFilter", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "profileId", + "description": "Filter by the object’s `profileId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taskId", + "description": "Filter by the object’s `taskId` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "IntFilter", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "WorkOnTaskInput", diff --git a/front/package.json b/front/package.json index f99d39c39..22ad2f2ce 100644 --- a/front/package.json +++ b/front/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "quasar build", "dev": "quasar dev", - "format": "prettier --write '**/*.{js,vue,graphql}'", + "format": "prettier --write '**/*.{ts,js,vue,graphql}'", "lint": "eslint --ext .js,.ts,.vue,.graphql ./", "test": "echo \"No test specified\" && exit 0", "codegen": "graphql-codegen --config codegen.yml" diff --git a/front/src/apollo/index.ts b/front/src/apollo/index.ts index ede061a86..c43d56a96 100644 --- a/front/src/apollo/index.ts +++ b/front/src/apollo/index.ts @@ -1,5 +1,11 @@ import type { ApolloClientOptions } from '@apollo/client/core'; -import { ApolloLink, from, InMemoryCache, split, TypePolicy } from '@apollo/client/core'; +import { + ApolloLink, + from, + InMemoryCache, + split, + TypePolicy, +} from '@apollo/client/core'; import { BatchHttpLink } from '@apollo/client/link/batch-http'; import { WebSocketLink } from '@apollo/client/link/ws'; import { getMainDefinition } from '@apollo/client/utilities'; @@ -7,7 +13,6 @@ import { createUploadLink } from 'apollo-upload-client'; import { extractFiles } from 'extract-files'; import { JWT_KEY } from 'src/ctfnote/auth'; - const protocol = document.location.protocol == 'https:' ? 'wss:' : 'ws:'; const wsLink = new WebSocketLink({ diff --git a/front/src/boot/ctfnote.ts b/front/src/boot/ctfnote.ts index bd838d2ce..ce1567940 100644 --- a/front/src/boot/ctfnote.ts +++ b/front/src/boot/ctfnote.ts @@ -22,7 +22,6 @@ export default boot(async ({ router, redirect, urlPath }) => { const route = router.resolve({ path }); let logged = false; - try { logged = !!(await ctfnote.auth.refreshJWT()); } catch { diff --git a/front/src/ctfnote/auth.ts b/front/src/ctfnote/auth.ts index 20d689e26..6aa7c97f5 100644 --- a/front/src/ctfnote/auth.ts +++ b/front/src/ctfnote/auth.ts @@ -5,12 +5,11 @@ import { useRegisterMutation, useRegisterWithPasswordMutation, useRegisterWithTokenMutation, - useResetPasswordMutation + useResetPasswordMutation, } from 'src/generated/graphql'; import { useRouter } from 'vue-router'; import { prefetchMe } from './me'; - export const JWT_KEY = 'JWT'; export function saveJWT(jwt: string | null | undefined) { @@ -49,7 +48,7 @@ export function useLogin() { const jwt = r?.data?.login?.jwt; if (jwt) { saveJWT(jwt); - await prefetchMe() + await prefetchMe(); await $router.push({ name: 'ctfs-incoming' }); } }; @@ -63,7 +62,7 @@ export function useRegister() { const jwt = r?.data?.register?.jwt; if (jwt) { saveJWT(jwt); - await prefetchMe() + await prefetchMe(); await $router.push({ name: 'ctfs-incoming' }); } }; @@ -77,7 +76,7 @@ export function useRegisterWithToken() { const jwt = r?.data?.registerWithToken?.jwt; if (jwt) { saveJWT(jwt); - await prefetchMe() + await prefetchMe(); await $router.push({ name: 'ctfs-incoming' }); } }; @@ -95,7 +94,7 @@ export function useRegisterWithPassword() { const jwt = r?.data?.registerWithPassword?.jwt; if (jwt) { saveJWT(jwt); - await prefetchMe() + await prefetchMe(); await $router.push({ name: 'ctfs-incoming' }); } }; @@ -109,7 +108,7 @@ export function useResetPassword() { const jwt = r?.data?.resetPassword?.jwt; if (jwt) { saveJWT(jwt); - await prefetchMe() + await prefetchMe(); await $router.push({ name: 'ctfs-incoming' }); } }; diff --git a/front/src/ctfnote/ctfs.ts b/front/src/ctfnote/ctfs.ts index 220e08f8e..06c4e221b 100644 --- a/front/src/ctfnote/ctfs.ts +++ b/front/src/ctfnote/ctfs.ts @@ -1,369 +1,369 @@ -import { date } from 'quasar'; -import slugify from 'slugify'; -import { - CtfFragment, - CtfInput, - CtfPatch, - CtfSecretFragment, - InvitationFragment, - SubscribeToCtfCreatedDocument, - SubscribeToCtfCreatedSubscription, - SubscribeToCtfCreatedSubscriptionVariables, - SubscribeToCtfDeletedDocument, - SubscribeToCtfDeletedSubscription, - SubscribeToCtfDeletedSubscriptionVariables, - TagFragment, - TaskFragment, - useCreateCtfMutation, - useCtfsQuery, - useDeleteCtfbyIdMutation, - useGetFullCtfQuery, - useImportctfMutation, - useIncomingCtfsQuery, - useInviteUserToCtfMutation, - usePastCtfsQuery, - useSetDiscordEventLinkMutation, - useSubscribeToCtfCreatedSubscription, - useSubscribeToCtfDeletedSubscription, - useSubscribeToCtfSubscription, - useSubscribeToFlagSubscription, - useSubscribeToTaskSubscription, - useUninviteUserToCtfMutation, - useUpdateCredentialsForCtfIdMutation, - useUpdateCtfByIdMutation, -} from 'src/generated/graphql'; -import { CtfInvitation, makeId } from './models'; -import { Ctf, Profile, Task } from './models'; -import { wrapQuery } from './utils'; -import { buildTag } from './tags'; -import { buildWorkingOn } from './tasks'; - -type FullCtfResponse = { - ctf: CtfFragment & { - tasks: { nodes: TaskFragment[] }; - secrets: CtfSecretFragment | null; - invitations: { nodes: InvitationFragment[] }; - }; -}; - -/* Builders */ - -export function safeSlugify(str: string) { - return slugify(str) || 'no-slug-for-you'; -} - -export function buildInvitation(invitation: InvitationFragment): CtfInvitation { - return { - ...invitation, - ctfId: makeId(invitation.ctfId), - profileId: makeId(invitation.profileId), - }; -} - -export function buildTask(task: TaskFragment): Task { - const slug = safeSlugify(task.title); - return { - ...task, - id: makeId(task.id), - ctfId: makeId(task.ctfId), - slug, - solved: task.solved ?? false, - workOnTasks: task.workOnTasks.nodes.map((w) => buildWorkingOn(w)), - assignedTags: task.assignedTags.nodes - .filter((t) => t.__typename && t.tag?.__typename) - .map((t) => buildTag(t.tag as TagFragment)), - }; -} - -function extractDate(d: string) { - const masks = [ - 'YYYY-MM-DDTHH:mm:ss.SSSZ', - 'YYYY-MM-DDTHH:mm:ss.SSZ', - 'YYYY-MM-DDTHH:mm:ss.SZ', - 'YYYY-MM-DDTHH:mm:ssZ', - ]; - for (const mask of masks) { - const r = date.extractDate(d, mask); - if (r.valueOf() > 0) { - return r; - } - } - throw 'invalid date'; -} - -export function buildCtf(ctf: CtfFragment): Ctf { - const slug = safeSlugify(ctf.title); - const params = { ctfId: ctf.id, ctfSlug: slug }; - const infoLink = { name: 'ctf-info', params }; - const tasksLink = { name: 'ctf-tasks', params }; - const guestsLink = { name: 'ctf-guests', params }; - - return { - ...ctf, - id: makeId(ctf.id), - ctfUrl: ctf.ctfUrl ?? null, - logoUrl: ctf.logoUrl ?? null, - ctftimeUrl: ctf.ctftimeUrl ?? null, - granted: ctf.granted ?? false, - credentials: null, - slug, - infoLink, - tasksLink, - guestsLink, - startTime: extractDate(ctf.startTime), - endTime: extractDate(ctf.endTime), - tasks: [], - invitations: [], - discordEventLink: ctf.discordEventLink ?? null, - }; -} - -export function buildFullCtf(data: FullCtfResponse): Ctf { - return { - ...buildCtf(data.ctf), - credentials: data.ctf.secrets?.credentials ?? null, - tasks: data.ctf.tasks.nodes.map(buildTask), - invitations: data.ctf.invitations.nodes.map(buildInvitation), - }; -} - -/* Queries */ - -export function getIncomingCtfs() { - const query = useIncomingCtfsQuery({ fetchPolicy: 'cache-and-network' }); - const wrappedQuery = wrapQuery(query, [], (data) => - data.incomingCtf.nodes.map(buildCtf) - ); - - /* Watch deletion */ - query.subscribeToMore< - SubscribeToCtfDeletedSubscriptionVariables, - SubscribeToCtfDeletedSubscription - >({ - document: SubscribeToCtfDeletedDocument, - updateQuery(oldResult, { subscriptionData }) { - const nodeId = subscriptionData.data.listen.relatedNodeId; - if (!nodeId) return oldResult; - const nodes = oldResult.incomingCtf?.nodes.slice() ?? []; - return { - incomingCtf: { - __typename: 'CtfsConnection', - nodes: nodes.filter((ctf) => ctf.nodeId != nodeId), - }, - }; - }, - }); - - /* Watch creation */ - query.subscribeToMore< - SubscribeToCtfCreatedSubscriptionVariables, - SubscribeToCtfCreatedSubscription - >({ - document: SubscribeToCtfCreatedDocument, - updateQuery(oldResult, { subscriptionData }) { - const node = subscriptionData.data.listen.relatedNode; - if (!node || node.__typename != 'Ctf') return oldResult; - const nodes = oldResult.incomingCtf?.nodes.slice() ?? []; - - const endTime = extractDate(node.endTime); - if (endTime > new Date()) { - nodes.push(node); - } - return { - incomingCtf: { - __typename: 'CtfsConnection', - nodes, - }, - }; - }, - }); - - return wrappedQuery; -} - -export function getPastCtfs(...args: Parameters) { - const query = usePastCtfsQuery(...args); - const wrappedQuery = wrapQuery(query, { total: 0, ctfs: [] }, (data) => ({ - total: data.pastCtf.totalCount, - ctfs: data.pastCtf.nodes.map(buildCtf), - })); - - /* Watch deletion */ - query.subscribeToMore< - SubscribeToCtfDeletedSubscriptionVariables, - SubscribeToCtfDeletedSubscription - >({ - document: SubscribeToCtfDeletedDocument, - updateQuery(oldResult, { subscriptionData }) { - const nodeId = subscriptionData.data.listen.relatedNodeId; - if (!nodeId) return oldResult; - const nodes = oldResult.pastCtf?.nodes.slice() ?? []; - const newNodes = nodes.filter((ctf) => ctf.nodeId != nodeId); - return { - pastCtf: { - __typename: 'CtfsConnection', - nodes: newNodes, - totalCount: (oldResult.pastCtf?.totalCount ?? 0) - 1, - }, - }; - }, - }); - - /* Watch creation */ - query.subscribeToMore< - SubscribeToCtfCreatedSubscriptionVariables, - SubscribeToCtfCreatedSubscription - >({ - document: SubscribeToCtfCreatedDocument, - updateQuery(oldResult, { subscriptionData }) { - const node = subscriptionData.data.listen.relatedNode; - if (!node || node.__typename != 'Ctf') return oldResult; - const nodes = oldResult.pastCtf?.nodes.slice() ?? []; - - const endTime = extractDate(node.endTime); - if (endTime < new Date()) { - nodes.push(node); - nodes.sort((a, b) => - Number(new Date(a.startTime)) > Number(new Date(b.startTime)) - ? -1 - : Number(new Date(a.startTime)) < Number(new Date(b.startTime)) - ? 1 - : 0 - ); - } - return { - pastCtf: { - __typename: 'CtfsConnection', - nodes, - totalCount: (oldResult.pastCtf?.totalCount ?? 0) + 1, - }, - }; - }, - }); - - return wrappedQuery; -} - -export function getCtf(...args: Parameters) { - const query = useGetFullCtfQuery(...args); - const wrappedQuery = wrapQuery(query, null, (data) => buildFullCtf(data)); - - return wrappedQuery; -} - -export function getAllCtfs() { - const query = useCtfsQuery(); - const wrappedQuery = wrapQuery(query, [], (data) => - data.ctfs.nodes.map(buildCtf) - ); - return wrappedQuery; -} - -/* Mutations */ - -export function useCreateCtf() { - const { mutate } = useCreateCtfMutation({}); - return (ctf: CtfInput) => mutate(ctf); -} - -export function useDeleteCtf() { - const { mutate } = useDeleteCtfbyIdMutation({}); - return (ctf: Ctf) => mutate({ id: ctf.id }); -} - -export function useUpdateCtf() { - const { mutate } = useUpdateCtfByIdMutation({}); - return (ctf: Ctf, patch: CtfPatch) => mutate({ id: ctf.id, ...patch }); -} - -export function useUpdateCtfCredentials() { - const { mutate } = useUpdateCredentialsForCtfIdMutation({}); - return (ctf: Ctf, credentials: string) => - mutate({ ctfId: ctf.id, credentials }); -} - -export function useImportCtf() { - const { mutate } = useImportctfMutation({}); - - return async (id: number) => mutate({ id }); -} - -export function useInviteUserToCtf() { - const { mutate } = useInviteUserToCtfMutation({}); - return (ctf: Ctf, profile: Profile) => - mutate({ ctfId: ctf.id, profileId: profile.id }); -} - -export function useUninviteUserToCtf() { - const { mutate } = useUninviteUserToCtfMutation({}); - return (ctf: Ctf, profile: Profile) => - mutate({ ctfId: ctf.id, profileId: profile.id }); -} - -export function useSetDiscordEventLink() { - const { mutate } = useSetDiscordEventLinkMutation({}); - return (ctf: Ctf, discordEventLink: string) => - mutate({ id: ctf.id, link: discordEventLink }); -} - -/* Subscription */ - -export function useOnFlag() { - const sub = useSubscribeToFlagSubscription(); - const onResult = function (cb: (task: Task) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Task') return; - cb(buildTask(node)); - }); - }; - return { ...sub, onResult }; -} - -export function useOnCtfUpdate() { - const sub = useSubscribeToCtfSubscription(); - const onResult = function (cb: (ctf: Ctf) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Ctf') return; - cb(buildCtf(node)); - }); - }; - return { ...sub, onResult }; -} - -export function useOnTaskUpdate() { - const sub = useSubscribeToTaskSubscription(); - const onResult = function (cb: (task: Task) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Task') return; - cb(buildTask(node)); - }); - }; - return { ...sub, onResult }; -} - -export function useOnCtfDeleted() { - const sub = useSubscribeToCtfDeletedSubscription(); - const onResult = function (cb: (ctf: Ctf) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Ctf') return; - cb(buildCtf(node)); - }); - }; - return { ...sub, onResult }; -} - -export function useOnCtfCreated() { - const sub = useSubscribeToCtfCreatedSubscription(); - const onResult = function (cb: (ctf: Ctf) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Ctf') return; - cb(buildCtf(node)); - }); - }; - return { ...sub, onResult }; -} +import { date } from 'quasar'; +import slugify from 'slugify'; +import { + CtfFragment, + CtfInput, + CtfPatch, + CtfSecretFragment, + InvitationFragment, + SubscribeToCtfCreatedDocument, + SubscribeToCtfCreatedSubscription, + SubscribeToCtfCreatedSubscriptionVariables, + SubscribeToCtfDeletedDocument, + SubscribeToCtfDeletedSubscription, + SubscribeToCtfDeletedSubscriptionVariables, + TagFragment, + TaskFragment, + useCreateCtfMutation, + useCtfsQuery, + useDeleteCtfbyIdMutation, + useGetFullCtfQuery, + useImportctfMutation, + useIncomingCtfsQuery, + useInviteUserToCtfMutation, + usePastCtfsQuery, + useSetDiscordEventLinkMutation, + useSubscribeToCtfCreatedSubscription, + useSubscribeToCtfDeletedSubscription, + useSubscribeToCtfSubscription, + useSubscribeToFlagSubscription, + useSubscribeToTaskSubscription, + useUninviteUserToCtfMutation, + useUpdateCredentialsForCtfIdMutation, + useUpdateCtfByIdMutation, +} from 'src/generated/graphql'; +import { CtfInvitation, makeId } from './models'; +import { Ctf, Profile, Task } from './models'; +import { wrapQuery } from './utils'; +import { buildTag } from './tags'; +import { buildWorkingOn } from './tasks'; + +type FullCtfResponse = { + ctf: CtfFragment & { + tasks: { nodes: TaskFragment[] }; + secrets: CtfSecretFragment | null; + invitations: { nodes: InvitationFragment[] }; + }; +}; + +/* Builders */ + +export function safeSlugify(str: string) { + return slugify(str) || 'no-slug-for-you'; +} + +export function buildInvitation(invitation: InvitationFragment): CtfInvitation { + return { + ...invitation, + ctfId: makeId(invitation.ctfId), + profileId: makeId(invitation.profileId), + }; +} + +export function buildTask(task: TaskFragment): Task { + const slug = safeSlugify(task.title); + return { + ...task, + id: makeId(task.id), + ctfId: makeId(task.ctfId), + slug, + solved: task.solved ?? false, + workOnTasks: task.workOnTasks.nodes.map((w) => buildWorkingOn(w)), + assignedTags: task.assignedTags.nodes + .filter((t) => t.__typename && t.tag?.__typename) + .map((t) => buildTag(t.tag as TagFragment)), + }; +} + +function extractDate(d: string) { + const masks = [ + 'YYYY-MM-DDTHH:mm:ss.SSSZ', + 'YYYY-MM-DDTHH:mm:ss.SSZ', + 'YYYY-MM-DDTHH:mm:ss.SZ', + 'YYYY-MM-DDTHH:mm:ssZ', + ]; + for (const mask of masks) { + const r = date.extractDate(d, mask); + if (r.valueOf() > 0) { + return r; + } + } + throw 'invalid date'; +} + +export function buildCtf(ctf: CtfFragment): Ctf { + const slug = safeSlugify(ctf.title); + const params = { ctfId: ctf.id, ctfSlug: slug }; + const infoLink = { name: 'ctf-info', params }; + const tasksLink = { name: 'ctf-tasks', params }; + const guestsLink = { name: 'ctf-guests', params }; + + return { + ...ctf, + id: makeId(ctf.id), + ctfUrl: ctf.ctfUrl ?? null, + logoUrl: ctf.logoUrl ?? null, + ctftimeUrl: ctf.ctftimeUrl ?? null, + granted: ctf.granted ?? false, + credentials: null, + slug, + infoLink, + tasksLink, + guestsLink, + startTime: extractDate(ctf.startTime), + endTime: extractDate(ctf.endTime), + tasks: [], + invitations: [], + discordEventLink: ctf.discordEventLink ?? null, + }; +} + +export function buildFullCtf(data: FullCtfResponse): Ctf { + return { + ...buildCtf(data.ctf), + credentials: data.ctf.secrets?.credentials ?? null, + tasks: data.ctf.tasks.nodes.map(buildTask), + invitations: data.ctf.invitations.nodes.map(buildInvitation), + }; +} + +/* Queries */ + +export function getIncomingCtfs() { + const query = useIncomingCtfsQuery({ fetchPolicy: 'cache-and-network' }); + const wrappedQuery = wrapQuery(query, [], (data) => + data.incomingCtf.nodes.map(buildCtf) + ); + + /* Watch deletion */ + query.subscribeToMore< + SubscribeToCtfDeletedSubscriptionVariables, + SubscribeToCtfDeletedSubscription + >({ + document: SubscribeToCtfDeletedDocument, + updateQuery(oldResult, { subscriptionData }) { + const nodeId = subscriptionData.data.listen.relatedNodeId; + if (!nodeId) return oldResult; + const nodes = oldResult.incomingCtf?.nodes.slice() ?? []; + return { + incomingCtf: { + __typename: 'CtfsConnection', + nodes: nodes.filter((ctf) => ctf.nodeId != nodeId), + }, + }; + }, + }); + + /* Watch creation */ + query.subscribeToMore< + SubscribeToCtfCreatedSubscriptionVariables, + SubscribeToCtfCreatedSubscription + >({ + document: SubscribeToCtfCreatedDocument, + updateQuery(oldResult, { subscriptionData }) { + const node = subscriptionData.data.listen.relatedNode; + if (!node || node.__typename != 'Ctf') return oldResult; + const nodes = oldResult.incomingCtf?.nodes.slice() ?? []; + + const endTime = extractDate(node.endTime); + if (endTime > new Date()) { + nodes.push(node); + } + return { + incomingCtf: { + __typename: 'CtfsConnection', + nodes, + }, + }; + }, + }); + + return wrappedQuery; +} + +export function getPastCtfs(...args: Parameters) { + const query = usePastCtfsQuery(...args); + const wrappedQuery = wrapQuery(query, { total: 0, ctfs: [] }, (data) => ({ + total: data.pastCtf.totalCount, + ctfs: data.pastCtf.nodes.map(buildCtf), + })); + + /* Watch deletion */ + query.subscribeToMore< + SubscribeToCtfDeletedSubscriptionVariables, + SubscribeToCtfDeletedSubscription + >({ + document: SubscribeToCtfDeletedDocument, + updateQuery(oldResult, { subscriptionData }) { + const nodeId = subscriptionData.data.listen.relatedNodeId; + if (!nodeId) return oldResult; + const nodes = oldResult.pastCtf?.nodes.slice() ?? []; + const newNodes = nodes.filter((ctf) => ctf.nodeId != nodeId); + return { + pastCtf: { + __typename: 'CtfsConnection', + nodes: newNodes, + totalCount: (oldResult.pastCtf?.totalCount ?? 0) - 1, + }, + }; + }, + }); + + /* Watch creation */ + query.subscribeToMore< + SubscribeToCtfCreatedSubscriptionVariables, + SubscribeToCtfCreatedSubscription + >({ + document: SubscribeToCtfCreatedDocument, + updateQuery(oldResult, { subscriptionData }) { + const node = subscriptionData.data.listen.relatedNode; + if (!node || node.__typename != 'Ctf') return oldResult; + const nodes = oldResult.pastCtf?.nodes.slice() ?? []; + + const endTime = extractDate(node.endTime); + if (endTime < new Date()) { + nodes.push(node); + nodes.sort((a, b) => + Number(new Date(a.startTime)) > Number(new Date(b.startTime)) + ? -1 + : Number(new Date(a.startTime)) < Number(new Date(b.startTime)) + ? 1 + : 0 + ); + } + return { + pastCtf: { + __typename: 'CtfsConnection', + nodes, + totalCount: (oldResult.pastCtf?.totalCount ?? 0) + 1, + }, + }; + }, + }); + + return wrappedQuery; +} + +export function getCtf(...args: Parameters) { + const query = useGetFullCtfQuery(...args); + const wrappedQuery = wrapQuery(query, null, (data) => buildFullCtf(data)); + + return wrappedQuery; +} + +export function getAllCtfs() { + const query = useCtfsQuery(); + const wrappedQuery = wrapQuery(query, [], (data) => + data.ctfs.nodes.map(buildCtf) + ); + return wrappedQuery; +} + +/* Mutations */ + +export function useCreateCtf() { + const { mutate } = useCreateCtfMutation({}); + return (ctf: CtfInput) => mutate(ctf); +} + +export function useDeleteCtf() { + const { mutate } = useDeleteCtfbyIdMutation({}); + return (ctf: Ctf) => mutate({ id: ctf.id }); +} + +export function useUpdateCtf() { + const { mutate } = useUpdateCtfByIdMutation({}); + return (ctf: Ctf, patch: CtfPatch) => mutate({ id: ctf.id, ...patch }); +} + +export function useUpdateCtfCredentials() { + const { mutate } = useUpdateCredentialsForCtfIdMutation({}); + return (ctf: Ctf, credentials: string) => + mutate({ ctfId: ctf.id, credentials }); +} + +export function useImportCtf() { + const { mutate } = useImportctfMutation({}); + + return async (id: number) => mutate({ id }); +} + +export function useInviteUserToCtf() { + const { mutate } = useInviteUserToCtfMutation({}); + return (ctf: Ctf, profile: Profile) => + mutate({ ctfId: ctf.id, profileId: profile.id }); +} + +export function useUninviteUserToCtf() { + const { mutate } = useUninviteUserToCtfMutation({}); + return (ctf: Ctf, profile: Profile) => + mutate({ ctfId: ctf.id, profileId: profile.id }); +} + +export function useSetDiscordEventLink() { + const { mutate } = useSetDiscordEventLinkMutation({}); + return (ctf: Ctf, discordEventLink: string) => + mutate({ id: ctf.id, link: discordEventLink }); +} + +/* Subscription */ + +export function useOnFlag() { + const sub = useSubscribeToFlagSubscription(); + const onResult = function (cb: (task: Task) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Task') return; + cb(buildTask(node)); + }); + }; + return { ...sub, onResult }; +} + +export function useOnCtfUpdate() { + const sub = useSubscribeToCtfSubscription(); + const onResult = function (cb: (ctf: Ctf) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Ctf') return; + cb(buildCtf(node)); + }); + }; + return { ...sub, onResult }; +} + +export function useOnTaskUpdate() { + const sub = useSubscribeToTaskSubscription(); + const onResult = function (cb: (task: Task) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Task') return; + cb(buildTask(node)); + }); + }; + return { ...sub, onResult }; +} + +export function useOnCtfDeleted() { + const sub = useSubscribeToCtfDeletedSubscription(); + const onResult = function (cb: (ctf: Ctf) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Ctf') return; + cb(buildCtf(node)); + }); + }; + return { ...sub, onResult }; +} + +export function useOnCtfCreated() { + const sub = useSubscribeToCtfCreatedSubscription(); + const onResult = function (cb: (ctf: Ctf) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Ctf') return; + cb(buildCtf(node)); + }); + }; + return { ...sub, onResult }; +} diff --git a/front/src/ctfnote/models.ts b/front/src/ctfnote/models.ts index 92be59c4d..f505310c8 100644 --- a/front/src/ctfnote/models.ts +++ b/front/src/ctfnote/models.ts @@ -1,133 +1,133 @@ -import { Role } from 'src/generated/graphql'; -export { Role } from 'src/generated/graphql'; -import { RouteLocationRaw } from 'vue-router'; - -/* Utils */ - -export type Id = number & { __type: T }; - -export function makeId(id: number): Id { - return id as Id; -} - -export type Maybe = T | null; - -/* CTFNote Types */ - -export type PublicProfile = { - id: Id; - username: string; - role: Role; - description: string; - color: string; - nodeId: string; -}; - -export type Profile = PublicProfile & { - lastactive: string; - discordId: string | null; -}; - -export type Me = { - profile: Profile; - - isLogged: boolean; - isGuest: boolean; - isMember: boolean; - isManager: boolean; - isAdmin: boolean; -}; - -export type Task = { - nodeId: string; - - ctfId: Id; - id: Id; - title: string; - padUrl: string; - slug: string; - description: string; - flag: string; - solved: boolean; - assignedTags: Tag[]; - workOnTasks: WorkingOn[]; - ctf?: Ctf | string; -}; - -export type CtfInvitation = { - nodeId: string; - - ctfId: Id; - profileId: Id; -}; - -export type Ctf = { - nodeId: string; - - id: Id; - title: string; - description: string; - startTime: Date; - endTime: Date; - weight: number; - slug: string; - infoLink: RouteLocationRaw; - tasksLink: RouteLocationRaw; - guestsLink: RouteLocationRaw; - granted: boolean; - ctfUrl: string | null; - ctftimeUrl: string | null; - logoUrl: string | null; - - credentials: string | null; - tasks: Task[]; - invitations: CtfInvitation[]; - discordEventLink: string | null; -}; - -export const defaultColorsNames = [ - 'primary', - 'secondary', - 'accent', - 'dark', - 'positive', - 'negative', - 'info', - 'warning', -] as const; - -export type SettingsColor = (typeof defaultColorsNames)[number]; -export type SettingsColorMap = Record; -export type Settings = { - registrationAllowed: boolean; - registrationPasswordAllowed: boolean; - style: SettingsColorMap; -}; - -export type AdminSettings = Settings & { - registrationPassword: string; - registrationDefaultRole: Role; - icalPassword: string; -}; - -export type User = { - nodeId: string; - - id: Id; - login: string; - role: Role; - profile: Profile; -}; - -export type Tag = { - nodeId: string; - id: Id; - tag: string; -}; - -export type WorkingOn = { - nodeId: string; - taskId: Id; - profileId: Id; - active: boolean; -}; +import { Role } from 'src/generated/graphql'; +export { Role } from 'src/generated/graphql'; +import { RouteLocationRaw } from 'vue-router'; + +/* Utils */ + +export type Id = number & { __type: T }; + +export function makeId(id: number): Id { + return id as Id; +} + +export type Maybe = T | null; + +/* CTFNote Types */ + +export type PublicProfile = { + id: Id; + username: string; + role: Role; + description: string; + color: string; + nodeId: string; +}; + +export type Profile = PublicProfile & { + lastactive: string; + discordId: string | null; +}; + +export type Me = { + profile: Profile; + + isLogged: boolean; + isGuest: boolean; + isMember: boolean; + isManager: boolean; + isAdmin: boolean; +}; + +export type Task = { + nodeId: string; + + ctfId: Id; + id: Id; + title: string; + padUrl: string; + slug: string; + description: string; + flag: string; + solved: boolean; + assignedTags: Tag[]; + workOnTasks: WorkingOn[]; + ctf?: Ctf | string; +}; + +export type CtfInvitation = { + nodeId: string; + + ctfId: Id; + profileId: Id; +}; + +export type Ctf = { + nodeId: string; + + id: Id; + title: string; + description: string; + startTime: Date; + endTime: Date; + weight: number; + slug: string; + infoLink: RouteLocationRaw; + tasksLink: RouteLocationRaw; + guestsLink: RouteLocationRaw; + granted: boolean; + ctfUrl: string | null; + ctftimeUrl: string | null; + logoUrl: string | null; + + credentials: string | null; + tasks: Task[]; + invitations: CtfInvitation[]; + discordEventLink: string | null; +}; + +export const defaultColorsNames = [ + 'primary', + 'secondary', + 'accent', + 'dark', + 'positive', + 'negative', + 'info', + 'warning', +] as const; + +export type SettingsColor = (typeof defaultColorsNames)[number]; +export type SettingsColorMap = Record; +export type Settings = { + registrationAllowed: boolean; + registrationPasswordAllowed: boolean; + style: SettingsColorMap; +}; + +export type AdminSettings = Settings & { + registrationPassword: string; + registrationDefaultRole: Role; + icalPassword: string; +}; + +export type User = { + nodeId: string; + + id: Id; + login: string; + role: Role; + profile: Profile; +}; + +export type Tag = { + nodeId: string; + id: Id; + tag: string; +}; + +export type WorkingOn = { + nodeId: string; + taskId: Id; + profileId: Id; + active: boolean; +}; diff --git a/front/src/ctfnote/profiles.ts b/front/src/ctfnote/profiles.ts index 7be8bb466..b27181feb 100644 --- a/front/src/ctfnote/profiles.ts +++ b/front/src/ctfnote/profiles.ts @@ -1,117 +1,117 @@ -import { - ProfileFragment, - PublicProfileFragment, - PublicProfileSubscriptionPayloadDocument, - Role, - useGetTeamAdminQuery, - useGetTeamQuery, - useSubscribeToProfileCreatedSubscription, -} from 'src/generated/graphql'; -import { makeId, Profile, PublicProfile } from './models'; -import { colorHash, wrapQuery } from './utils'; -import { Ref, InjectionKey, provide, inject } from 'vue'; - -/* Builders */ -// type FullPublicProfileFragement = { -// [k in keyof PublicProfileFragment]-?: Required< -// NonNullable -// >; -// }; - -export function buildPublicProfile(p: PublicProfileFragment): PublicProfile { - // These checks are here because PublicProfile comes from a view - // which does not have nullability checks - if (p.username == null) throw new Error("Username can't be null"); - if (p.id == null) throw new Error("ID can't be null"); - if (p.nodeId == null) throw new Error("NodeID can't be null"); - const id = p.id; - const username = p.username; - const nodeId = p.nodeId; - - return { - ...p, - nodeId: nodeId, - username: username, - description: p.description ? p.description : '', - color: p.color ?? colorHash(username), - id: makeId(id), - role: p.role as Role, - }; -} - -export function buildPublicProfileFromProfile(p: Profile): PublicProfile { - return { - ...p, - color: p.color ?? colorHash(p.username), - id: makeId(p.id), - role: p.role, - }; -} - -export function buildProfile(p: ProfileFragment): Profile { - return { - ...p, - discordId: p.discordId ?? null, - lastactive: p.lastactive, - color: p.color ?? colorHash(p.username), - id: makeId(p.id), - role: p.role as Role, - }; -} - -/* Global provider */ - -const TeamSymbol: InjectionKey> = Symbol('team'); - -export function provideTeam() { - const { result: team } = getTeam(); - provide(TeamSymbol, team); - return team; -} - -export function injectTeam() { - const team = inject(TeamSymbol); - if (!team) { - throw 'ERROR'; - } - - return team; -} - -/* Queries */ - -export function getTeam() { - const query = useGetTeamQuery(); - const wrappedQuery = wrapQuery( - query, - [], - (data) => data.publicProfiles?.nodes.map(buildPublicProfile) ?? [] - ); - - query.subscribeToMore({ document: PublicProfileSubscriptionPayloadDocument }); - return wrappedQuery; -} - -export function getTeamAdmin() { - const query = useGetTeamAdminQuery(); - const wrappedQuery = wrapQuery( - query, - [], - (data) => data.profiles?.nodes.map(buildProfile) ?? [] - ); - - return wrappedQuery; -} - -/* Subcriptions */ -export function useOnProfileCreated() { - const sub = useSubscribeToProfileCreatedSubscription(); - const onResult = function (cb: (profile: Profile) => void) { - sub.onResult((data) => { - const node = data.data?.listen.relatedNode; - if (!node || node.__typename != 'Profile') return; - cb(buildProfile(node)); - }); - }; - return { ...sub, onResult }; -} +import { + ProfileFragment, + PublicProfileFragment, + PublicProfileSubscriptionPayloadDocument, + Role, + useGetTeamAdminQuery, + useGetTeamQuery, + useSubscribeToProfileCreatedSubscription, +} from 'src/generated/graphql'; +import { makeId, Profile, PublicProfile } from './models'; +import { colorHash, wrapQuery } from './utils'; +import { Ref, InjectionKey, provide, inject } from 'vue'; + +/* Builders */ +// type FullPublicProfileFragement = { +// [k in keyof PublicProfileFragment]-?: Required< +// NonNullable +// >; +// }; + +export function buildPublicProfile(p: PublicProfileFragment): PublicProfile { + // These checks are here because PublicProfile comes from a view + // which does not have nullability checks + if (p.username == null) throw new Error("Username can't be null"); + if (p.id == null) throw new Error("ID can't be null"); + if (p.nodeId == null) throw new Error("NodeID can't be null"); + const id = p.id; + const username = p.username; + const nodeId = p.nodeId; + + return { + ...p, + nodeId: nodeId, + username: username, + description: p.description ? p.description : '', + color: p.color ?? colorHash(username), + id: makeId(id), + role: p.role as Role, + }; +} + +export function buildPublicProfileFromProfile(p: Profile): PublicProfile { + return { + ...p, + color: p.color ?? colorHash(p.username), + id: makeId(p.id), + role: p.role, + }; +} + +export function buildProfile(p: ProfileFragment): Profile { + return { + ...p, + discordId: p.discordId ?? null, + lastactive: p.lastactive, + color: p.color ?? colorHash(p.username), + id: makeId(p.id), + role: p.role as Role, + }; +} + +/* Global provider */ + +const TeamSymbol: InjectionKey> = Symbol('team'); + +export function provideTeam() { + const { result: team } = getTeam(); + provide(TeamSymbol, team); + return team; +} + +export function injectTeam() { + const team = inject(TeamSymbol); + if (!team) { + throw 'ERROR'; + } + + return team; +} + +/* Queries */ + +export function getTeam() { + const query = useGetTeamQuery(); + const wrappedQuery = wrapQuery( + query, + [], + (data) => data.publicProfiles?.nodes.map(buildPublicProfile) ?? [] + ); + + query.subscribeToMore({ document: PublicProfileSubscriptionPayloadDocument }); + return wrappedQuery; +} + +export function getTeamAdmin() { + const query = useGetTeamAdminQuery(); + const wrappedQuery = wrapQuery( + query, + [], + (data) => data.profiles?.nodes.map(buildProfile) ?? [] + ); + + return wrappedQuery; +} + +/* Subcriptions */ +export function useOnProfileCreated() { + const sub = useSubscribeToProfileCreatedSubscription(); + const onResult = function (cb: (profile: Profile) => void) { + sub.onResult((data) => { + const node = data.data?.listen.relatedNode; + if (!node || node.__typename != 'Profile') return; + cb(buildProfile(node)); + }); + }; + return { ...sub, onResult }; +} diff --git a/front/src/ctfnote/tags.ts b/front/src/ctfnote/tags.ts index 3e68df706..682cd332f 100644 --- a/front/src/ctfnote/tags.ts +++ b/front/src/ctfnote/tags.ts @@ -1,68 +1,68 @@ -/* Builders */ -import { - TagFragment, - useAddTagsForTaskMutation, - useGetTagsQuery, -} from 'src/generated/graphql'; -import { Id, makeId, Tag, Task } from './models'; -import { wrapQuery } from './utils'; -import { Ref, InjectionKey, provide, inject } from 'vue'; - -export function buildTag(t: TagFragment): Tag { - return { - ...t, - id: makeId(t.id), - }; -} - -export function tagsSortFn(a: Task, b: Task): number { - for (const tagA of a.assignedTags) { - for (const tagB of b.assignedTags) { - const result = tagA.tag - .toLocaleLowerCase() - .localeCompare(tagB.tag.toLocaleLowerCase()); - if (result != 0) { - return result; - } - } - } - - return 0; -} - -/* Global provider */ -const TagsSymbol: InjectionKey> = Symbol('tags'); - -export function provideTags() { - const { result: tags } = getTags(); - provide(TagsSymbol, tags); - return tags; -} - -export function injectTags() { - const tags = inject(TagsSymbol); - if (!tags) { - throw 'ERROR'; - } - - return tags; -} - -/* Queries */ -export function getTags() { - const query = useGetTagsQuery(); - const wrappedQuery = wrapQuery( - query, - [], - (data) => data.tags?.nodes.map(buildTag) ?? [] - ); - - return wrappedQuery; -} - -/* Mutations */ -export function useAddTagsForTask() { - const { mutate: addTagsForTask } = useAddTagsForTaskMutation({}); - return (tags: Array, taskId: Id) => - addTagsForTask({ tags: tags, taskId: taskId }); -} +/* Builders */ +import { + TagFragment, + useAddTagsForTaskMutation, + useGetTagsQuery, +} from 'src/generated/graphql'; +import { Id, makeId, Tag, Task } from './models'; +import { wrapQuery } from './utils'; +import { Ref, InjectionKey, provide, inject } from 'vue'; + +export function buildTag(t: TagFragment): Tag { + return { + ...t, + id: makeId(t.id), + }; +} + +export function tagsSortFn(a: Task, b: Task): number { + for (const tagA of a.assignedTags) { + for (const tagB of b.assignedTags) { + const result = tagA.tag + .toLocaleLowerCase() + .localeCompare(tagB.tag.toLocaleLowerCase()); + if (result != 0) { + return result; + } + } + } + + return 0; +} + +/* Global provider */ +const TagsSymbol: InjectionKey> = Symbol('tags'); + +export function provideTags() { + const { result: tags } = getTags(); + provide(TagsSymbol, tags); + return tags; +} + +export function injectTags() { + const tags = inject(TagsSymbol); + if (!tags) { + throw 'ERROR'; + } + + return tags; +} + +/* Queries */ +export function getTags() { + const query = useGetTagsQuery(); + const wrappedQuery = wrapQuery( + query, + [], + (data) => data.tags?.nodes.map(buildTag) ?? [] + ); + + return wrappedQuery; +} + +/* Mutations */ +export function useAddTagsForTask() { + const { mutate: addTagsForTask } = useAddTagsForTaskMutation({}); + return (tags: Array, taskId: Id) => + addTagsForTask({ tags: tags, taskId: taskId }); +} diff --git a/front/src/ctfnote/tasks.ts b/front/src/ctfnote/tasks.ts index 9f729d00c..8c945105a 100644 --- a/front/src/ctfnote/tasks.ts +++ b/front/src/ctfnote/tasks.ts @@ -1,118 +1,118 @@ -import { - CreateTaskInput, - TaskPatch, - WorkingOnFragment, - useCancelWorkingOnMutation, - useCreateTaskForCtfIdMutation, - useDeleteTaskMutation, - useStartWorkingOnMutation, - useStopWorkingOnMutation, - useUpdateTaskMutation, -} from 'src/generated/graphql'; - -import { Ctf, Id, Task, WorkingOn, makeId } from './models'; -import { Dialog } from 'quasar'; -import TaskEditDialogVue from '../components/Dialogs/TaskEditDialog.vue'; -import TaskSolveDialogVue from '../components/Dialogs/TaskSolveDialog.vue'; -import { ref, computed } from 'vue'; - -export function buildWorkingOn(w: WorkingOnFragment): WorkingOn { - return { - ...w, - taskId: makeId(w.taskId), - profileId: makeId(w.profileId), - }; -} - -/* Mutations */ -export function useCreateTask() { - const { mutate: doCreateTask } = useCreateTaskForCtfIdMutation({}); - return (ctfId: Id, task: Omit) => - doCreateTask({ ...task, ctfId }); -} - -export function useDeleteTask() { - const { mutate: doDeleteTask } = useDeleteTaskMutation({}); - return (task: Task) => doDeleteTask({ id: task.id }); -} - -export function useUpdateTask() { - const { mutate: doUpdateTask } = useUpdateTaskMutation({}); - return (task: Task, patch: TaskPatch) => - doUpdateTask({ id: task.id, ...patch }); -} - -export function useStartWorkingOn() { - const { mutate: doStartWorking } = useStartWorkingOnMutation({}); - return (task: Task) => doStartWorking({ taskId: task.id }); -} - -export function useStopWorkingOn() { - const { mutate: doStopWorking } = useStopWorkingOnMutation({}); - return (task: Task) => doStopWorking({ taskId: task.id }); -} - -export function useCancelWorkingOn() { - const { mutate: doCancelWorking } = useCancelWorkingOnMutation({}); - return (task: Task) => doCancelWorking({ taskId: task.id }); -} - -export function useSolveTaskPopup() { - // Used to force opening at most one dialog at a time - const openedSolveTaskPopup = ref(false); - - const lock = () => (openedSolveTaskPopup.value = true); - const unlock = () => (openedSolveTaskPopup.value = false); - const locked = computed(() => openedSolveTaskPopup.value); - - return (task: Task) => { - // If the dialog is already opened, don't do anything - if (locked.value) return; - - lock(); - - Dialog.create({ - component: TaskSolveDialogVue, - componentProps: { - task, - }, - }) - .onOk(unlock) - .onCancel(unlock) - .onDismiss(unlock); - }; -} - -export function useDeleteTaskPopup() { - const deleteTask = useDeleteTask(); - return (task: Task) => { - Dialog.create({ - title: `Delete ${task.title}?`, - color: 'primary', - class: 'compact-dialog', - message: 'This will delete the task, but not the pads.', - cancel: { - label: 'Cancel', - flat: true, - }, - ok: { - color: 'negative', - label: 'Delete', - flat: true, - }, - }).onOk(() => { - void deleteTask(task); - }); - }; -} - -export function useEditTaskPopup() { - return (task: Task) => { - Dialog.create({ - component: TaskEditDialogVue, - componentProps: { - task, - }, - }); - }; -} +import { + CreateTaskInput, + TaskPatch, + WorkingOnFragment, + useCancelWorkingOnMutation, + useCreateTaskForCtfIdMutation, + useDeleteTaskMutation, + useStartWorkingOnMutation, + useStopWorkingOnMutation, + useUpdateTaskMutation, +} from 'src/generated/graphql'; + +import { Ctf, Id, Task, WorkingOn, makeId } from './models'; +import { Dialog } from 'quasar'; +import TaskEditDialogVue from '../components/Dialogs/TaskEditDialog.vue'; +import TaskSolveDialogVue from '../components/Dialogs/TaskSolveDialog.vue'; +import { ref, computed } from 'vue'; + +export function buildWorkingOn(w: WorkingOnFragment): WorkingOn { + return { + ...w, + taskId: makeId(w.taskId), + profileId: makeId(w.profileId), + }; +} + +/* Mutations */ +export function useCreateTask() { + const { mutate: doCreateTask } = useCreateTaskForCtfIdMutation({}); + return (ctfId: Id, task: Omit) => + doCreateTask({ ...task, ctfId }); +} + +export function useDeleteTask() { + const { mutate: doDeleteTask } = useDeleteTaskMutation({}); + return (task: Task) => doDeleteTask({ id: task.id }); +} + +export function useUpdateTask() { + const { mutate: doUpdateTask } = useUpdateTaskMutation({}); + return (task: Task, patch: TaskPatch) => + doUpdateTask({ id: task.id, ...patch }); +} + +export function useStartWorkingOn() { + const { mutate: doStartWorking } = useStartWorkingOnMutation({}); + return (task: Task) => doStartWorking({ taskId: task.id }); +} + +export function useStopWorkingOn() { + const { mutate: doStopWorking } = useStopWorkingOnMutation({}); + return (task: Task) => doStopWorking({ taskId: task.id }); +} + +export function useCancelWorkingOn() { + const { mutate: doCancelWorking } = useCancelWorkingOnMutation({}); + return (task: Task) => doCancelWorking({ taskId: task.id }); +} + +export function useSolveTaskPopup() { + // Used to force opening at most one dialog at a time + const openedSolveTaskPopup = ref(false); + + const lock = () => (openedSolveTaskPopup.value = true); + const unlock = () => (openedSolveTaskPopup.value = false); + const locked = computed(() => openedSolveTaskPopup.value); + + return (task: Task) => { + // If the dialog is already opened, don't do anything + if (locked.value) return; + + lock(); + + Dialog.create({ + component: TaskSolveDialogVue, + componentProps: { + task, + }, + }) + .onOk(unlock) + .onCancel(unlock) + .onDismiss(unlock); + }; +} + +export function useDeleteTaskPopup() { + const deleteTask = useDeleteTask(); + return (task: Task) => { + Dialog.create({ + title: `Delete ${task.title}?`, + color: 'primary', + class: 'compact-dialog', + message: 'This will delete the task, but not the pads.', + cancel: { + label: 'Cancel', + flat: true, + }, + ok: { + color: 'negative', + label: 'Delete', + flat: true, + }, + }).onOk(() => { + void deleteTask(task); + }); + }; +} + +export function useEditTaskPopup() { + return (task: Task) => { + Dialog.create({ + component: TaskEditDialogVue, + componentProps: { + task, + }, + }); + }; +} diff --git a/front/src/ctfnote/ui.ts b/front/src/ctfnote/ui.ts index 5e295685e..993777d6a 100644 --- a/front/src/ctfnote/ui.ts +++ b/front/src/ctfnote/ui.ts @@ -2,12 +2,12 @@ import { QVueGlobals, useQuasar } from 'quasar'; type NotifyOptions = Exclude[0], string> & { message: string; - + /** * Unique tag used to prevent duplicate notifications. * @type{string} * */ - tag?: string; + tag?: string; }; const USE_SYSTEM_NOTIFICATION = 'use-system-notification'; diff --git a/front/src/generated/graphql.ts b/front/src/generated/graphql.ts index a39d737fc..a2d6744e1 100644 --- a/front/src/generated/graphql.ts +++ b/front/src/generated/graphql.ts @@ -67,6 +67,20 @@ export type AssignedTagCondition = { taskId?: InputMaybe; }; +/** A filter to be used against `AssignedTag` object types. All fields are combined with a logical ‘and.’ */ +export type AssignedTagFilter = { + /** Checks for all expressions in this list. */ + and?: InputMaybe>; + /** Negates the expression. */ + not?: InputMaybe; + /** Checks for any expressions in this list. */ + or?: InputMaybe>; + /** Filter by the object’s `tagId` field. */ + tagId?: InputMaybe; + /** Filter by the object’s `taskId` field. */ + taskId?: InputMaybe; +}; + /** An input for mutations affecting `AssignedTag` */ export type AssignedTagInput = { tagId: Scalars['Int']; @@ -106,6 +120,32 @@ export enum AssignedTagsOrderBy { TaskIdDesc = 'TASK_ID_DESC' } +/** A filter to be used against Boolean fields. All fields are combined with a logical ‘and.’ */ +export type BooleanFilter = { + /** Not equal to the specified value, treating null like an ordinary value. */ + distinctFrom?: InputMaybe; + /** Equal to the specified value. */ + equalTo?: InputMaybe; + /** Greater than the specified value. */ + greaterThan?: InputMaybe; + /** Greater than or equal to the specified value. */ + greaterThanOrEqualTo?: InputMaybe; + /** Included in the specified list. */ + in?: InputMaybe>; + /** Is null (if `true` is specified) or is not null (if `false` is specified). */ + isNull?: InputMaybe; + /** Less than the specified value. */ + lessThan?: InputMaybe; + /** Less than or equal to the specified value. */ + lessThanOrEqualTo?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value. */ + notDistinctFrom?: InputMaybe; + /** Not equal to the specified value. */ + notEqualTo?: InputMaybe; + /** Not included in the specified list. */ + notIn?: InputMaybe>; +}; + /** All input for the `cancelWorkingOn` mutation. */ export type CancelWorkingOnInput = { /** @@ -440,6 +480,7 @@ export type CtfInvitationsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -488,10 +529,20 @@ export type CtfCondition = { export type CtfFilter = { /** Checks for all expressions in this list. */ and?: InputMaybe>; + /** Filter by the object’s `endTime` field. */ + endTime?: InputMaybe; + /** Filter by the object’s `granted` field. */ + granted?: InputMaybe; + /** Filter by the object’s `id` field. */ + id?: InputMaybe; /** Negates the expression. */ not?: InputMaybe; /** Checks for any expressions in this list. */ or?: InputMaybe>; + /** Filter by the object’s `secretsId` field. */ + secretsId?: InputMaybe; + /** Filter by the object’s `startTime` field. */ + startTime?: InputMaybe; /** Filter by the object’s `title` field. */ title?: InputMaybe; }; @@ -574,6 +625,18 @@ export type CtfSecretCondition = { id?: InputMaybe; }; +/** A filter to be used against `CtfSecret` object types. All fields are combined with a logical ‘and.’ */ +export type CtfSecretFilter = { + /** Checks for all expressions in this list. */ + and?: InputMaybe>; + /** Filter by the object’s `id` field. */ + id?: InputMaybe; + /** Negates the expression. */ + not?: InputMaybe; + /** Checks for any expressions in this list. */ + or?: InputMaybe>; +}; + /** Represents an update to a `CtfSecret`. Fields that are set will be updated. */ export type CtfSecretPatch = { credentials?: InputMaybe; @@ -649,6 +712,32 @@ export enum CtfsOrderBy { TitleDesc = 'TITLE_DESC' } +/** A filter to be used against Datetime fields. All fields are combined with a logical ‘and.’ */ +export type DatetimeFilter = { + /** Not equal to the specified value, treating null like an ordinary value. */ + distinctFrom?: InputMaybe; + /** Equal to the specified value. */ + equalTo?: InputMaybe; + /** Greater than the specified value. */ + greaterThan?: InputMaybe; + /** Greater than or equal to the specified value. */ + greaterThanOrEqualTo?: InputMaybe; + /** Included in the specified list. */ + in?: InputMaybe>; + /** Is null (if `true` is specified) or is not null (if `false` is specified). */ + isNull?: InputMaybe; + /** Less than the specified value. */ + lessThan?: InputMaybe; + /** Less than or equal to the specified value. */ + lessThanOrEqualTo?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value. */ + notDistinctFrom?: InputMaybe; + /** Not equal to the specified value. */ + notEqualTo?: InputMaybe; + /** Not included in the specified list. */ + notIn?: InputMaybe>; +}; + /** All input for the `deleteAssignedTagByNodeId` mutation. */ export type DeleteAssignedTagByNodeIdInput = { /** @@ -921,6 +1010,32 @@ export type ImportCtfPayload = { query?: Maybe; }; +/** A filter to be used against Int fields. All fields are combined with a logical ‘and.’ */ +export type IntFilter = { + /** Not equal to the specified value, treating null like an ordinary value. */ + distinctFrom?: InputMaybe; + /** Equal to the specified value. */ + equalTo?: InputMaybe; + /** Greater than the specified value. */ + greaterThan?: InputMaybe; + /** Greater than or equal to the specified value. */ + greaterThanOrEqualTo?: InputMaybe; + /** Included in the specified list. */ + in?: InputMaybe>; + /** Is null (if `true` is specified) or is not null (if `false` is specified). */ + isNull?: InputMaybe; + /** Less than the specified value. */ + lessThan?: InputMaybe; + /** Less than or equal to the specified value. */ + lessThanOrEqualTo?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value. */ + notDistinctFrom?: InputMaybe; + /** Not equal to the specified value. */ + notEqualTo?: InputMaybe; + /** Not included in the specified list. */ + notIn?: InputMaybe>; +}; + export type Invitation = Node & { __typename?: 'Invitation'; /** Reads a single `Ctf` that is related to this `Invitation`. */ @@ -944,6 +1059,20 @@ export type InvitationCondition = { profileId?: InputMaybe; }; +/** A filter to be used against `Invitation` object types. All fields are combined with a logical ‘and.’ */ +export type InvitationFilter = { + /** Checks for all expressions in this list. */ + and?: InputMaybe>; + /** Filter by the object’s `ctfId` field. */ + ctfId?: InputMaybe; + /** Negates the expression. */ + not?: InputMaybe; + /** Checks for any expressions in this list. */ + or?: InputMaybe>; + /** Filter by the object’s `profileId` field. */ + profileId?: InputMaybe; +}; + /** An input for mutations affecting `Invitation` */ export type InvitationInput = { ctfId: Scalars['Int']; @@ -1453,6 +1582,7 @@ export type ProfileInvitationsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1476,6 +1606,7 @@ export type ProfileWorkOnTasksArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1520,10 +1651,14 @@ export type ProfileFilter = { and?: InputMaybe>; /** Filter by the object’s `discordId` field. */ discordId?: InputMaybe; + /** Filter by the object’s `id` field. */ + id?: InputMaybe; /** Negates the expression. */ not?: InputMaybe; /** Checks for any expressions in this list. */ or?: InputMaybe>; + /** Filter by the object’s `role` field. */ + role?: InputMaybe; /** Filter by the object’s `username` field. */ username?: InputMaybe; }; @@ -1729,6 +1864,7 @@ export type QueryAssignedTagsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1765,6 +1901,7 @@ export type QueryCtfSecretsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1789,6 +1926,7 @@ export type QueryCtfsArgs = { export type QueryGuestsArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1799,6 +1937,7 @@ export type QueryGuestsArgs = { export type QueryIncomingCtfArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1823,6 +1962,7 @@ export type QueryInvitationsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1840,6 +1980,7 @@ export type QueryNodeArgs = { export type QueryPastCtfArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -1996,6 +2137,7 @@ export type QueryWorkOnTasksArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -2157,6 +2299,32 @@ export enum Role { UserMember = 'USER_MEMBER' } +/** A filter to be used against Role fields. All fields are combined with a logical ‘and.’ */ +export type RoleFilter = { + /** Not equal to the specified value, treating null like an ordinary value. */ + distinctFrom?: InputMaybe; + /** Equal to the specified value. */ + equalTo?: InputMaybe; + /** Greater than the specified value. */ + greaterThan?: InputMaybe; + /** Greater than or equal to the specified value. */ + greaterThanOrEqualTo?: InputMaybe; + /** Included in the specified list. */ + in?: InputMaybe>; + /** Is null (if `true` is specified) or is not null (if `false` is specified). */ + isNull?: InputMaybe; + /** Less than the specified value. */ + lessThan?: InputMaybe; + /** Less than or equal to the specified value. */ + lessThanOrEqualTo?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value. */ + notDistinctFrom?: InputMaybe; + /** Not equal to the specified value. */ + notEqualTo?: InputMaybe; + /** Not included in the specified list. */ + notIn?: InputMaybe>; +}; + /** All input for the `setDiscordEventLink` mutation. */ export type SetDiscordEventLinkInput = { /** @@ -2303,8 +2471,80 @@ export type StopWorkingOnPayloadWorkOnTaskEdgeArgs = { /** A filter to be used against String fields. All fields are combined with a logical ‘and.’ */ export type StringFilter = { + /** Not equal to the specified value, treating null like an ordinary value. */ + distinctFrom?: InputMaybe; + /** Not equal to the specified value, treating null like an ordinary value (case-insensitive). */ + distinctFromInsensitive?: InputMaybe; + /** Ends with the specified string (case-sensitive). */ + endsWith?: InputMaybe; + /** Ends with the specified string (case-insensitive). */ + endsWithInsensitive?: InputMaybe; + /** Equal to the specified value. */ + equalTo?: InputMaybe; + /** Equal to the specified value (case-insensitive). */ + equalToInsensitive?: InputMaybe; + /** Greater than the specified value. */ + greaterThan?: InputMaybe; + /** Greater than the specified value (case-insensitive). */ + greaterThanInsensitive?: InputMaybe; + /** Greater than or equal to the specified value. */ + greaterThanOrEqualTo?: InputMaybe; + /** Greater than or equal to the specified value (case-insensitive). */ + greaterThanOrEqualToInsensitive?: InputMaybe; + /** Included in the specified list. */ + in?: InputMaybe>; + /** Included in the specified list (case-insensitive). */ + inInsensitive?: InputMaybe>; + /** Contains the specified string (case-sensitive). */ + includes?: InputMaybe; /** Contains the specified string (case-insensitive). */ includesInsensitive?: InputMaybe; + /** Is null (if `true` is specified) or is not null (if `false` is specified). */ + isNull?: InputMaybe; + /** Less than the specified value. */ + lessThan?: InputMaybe; + /** Less than the specified value (case-insensitive). */ + lessThanInsensitive?: InputMaybe; + /** Less than or equal to the specified value. */ + lessThanOrEqualTo?: InputMaybe; + /** Less than or equal to the specified value (case-insensitive). */ + lessThanOrEqualToInsensitive?: InputMaybe; + /** Matches the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. */ + like?: InputMaybe; + /** Matches the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. */ + likeInsensitive?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value. */ + notDistinctFrom?: InputMaybe; + /** Equal to the specified value, treating null like an ordinary value (case-insensitive). */ + notDistinctFromInsensitive?: InputMaybe; + /** Does not end with the specified string (case-sensitive). */ + notEndsWith?: InputMaybe; + /** Does not end with the specified string (case-insensitive). */ + notEndsWithInsensitive?: InputMaybe; + /** Not equal to the specified value. */ + notEqualTo?: InputMaybe; + /** Not equal to the specified value (case-insensitive). */ + notEqualToInsensitive?: InputMaybe; + /** Not included in the specified list. */ + notIn?: InputMaybe>; + /** Not included in the specified list (case-insensitive). */ + notInInsensitive?: InputMaybe>; + /** Does not contain the specified string (case-sensitive). */ + notIncludes?: InputMaybe; + /** Does not contain the specified string (case-insensitive). */ + notIncludesInsensitive?: InputMaybe; + /** Does not match the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. */ + notLike?: InputMaybe; + /** Does not match the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. */ + notLikeInsensitive?: InputMaybe; + /** Does not start with the specified string (case-sensitive). */ + notStartsWith?: InputMaybe; + /** Does not start with the specified string (case-insensitive). */ + notStartsWithInsensitive?: InputMaybe; + /** Starts with the specified string (case-sensitive). */ + startsWith?: InputMaybe; + /** Starts with the specified string (case-insensitive). */ + startsWithInsensitive?: InputMaybe; }; /** The root subscription type: contains realtime events you can subscribe to with the `subscription` operation. */ @@ -2339,6 +2579,7 @@ export type TagAssignedTagsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -2369,6 +2610,8 @@ export type TagCondition = { export type TagFilter = { /** Checks for all expressions in this list. */ and?: InputMaybe>; + /** Filter by the object’s `id` field. */ + id?: InputMaybe; /** Negates the expression. */ not?: InputMaybe; /** Checks for any expressions in this list. */ @@ -2466,6 +2709,7 @@ export type TaskAssignedTagsArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -2501,6 +2745,7 @@ export type TaskWorkOnTasksArgs = { after?: InputMaybe; before?: InputMaybe; condition?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; offset?: InputMaybe; @@ -2521,10 +2766,16 @@ export type TaskCondition = { export type TaskFilter = { /** Checks for all expressions in this list. */ and?: InputMaybe>; + /** Filter by the object’s `ctfId` field. */ + ctfId?: InputMaybe; + /** Filter by the object’s `id` field. */ + id?: InputMaybe; /** Negates the expression. */ not?: InputMaybe; /** Checks for any expressions in this list. */ or?: InputMaybe>; + /** Filter by the object’s `solved` field. */ + solved?: InputMaybe; /** Filter by the object’s `title` field. */ title?: InputMaybe; }; @@ -3031,6 +3282,20 @@ export type WorkOnTaskCondition = { taskId?: InputMaybe; }; +/** A filter to be used against `WorkOnTask` object types. All fields are combined with a logical ‘and.’ */ +export type WorkOnTaskFilter = { + /** Checks for all expressions in this list. */ + and?: InputMaybe>; + /** Negates the expression. */ + not?: InputMaybe; + /** Checks for any expressions in this list. */ + or?: InputMaybe>; + /** Filter by the object’s `profileId` field. */ + profileId?: InputMaybe; + /** Filter by the object’s `taskId` field. */ + taskId?: InputMaybe; +}; + /** An input for mutations affecting `WorkOnTask` */ export type WorkOnTaskInput = { active?: InputMaybe; diff --git a/front/src/router/index.ts b/front/src/router/index.ts index d92342471..fdf9cabbc 100644 --- a/front/src/router/index.ts +++ b/front/src/router/index.ts @@ -19,7 +19,9 @@ import routes from './routes'; export default route(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER ? createMemoryHistory - : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory); + : process.env.VUE_ROUTER_MODE === 'history' + ? createWebHistory + : createWebHashHistory; const Router = createRouter({ scrollBehavior: () => ({ left: 0, top: 0 }),