diff --git a/assets/openapi.json b/assets/openapi.json index 095d60d..aa965de 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -205,37 +205,6 @@ "type" ] }, -<<<<<<< HEAD - "AckBulkSchema": { - "type": "object", - "properties": { - "read_states": { - "type": "array", - "items": { - "type": "object", - "properties": { - "channel_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "read_state_type": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "channel_id", - "message_id", - "read_state_type" - ] - } - } - }, - "required": [ - "read_states" -======= "RouteResponse": { "type": "object", "properties": { @@ -268,7 +237,6 @@ "code", "errors", "message" ->>>>>>> 288a50d8d (Clean up imports in schemas/api/bots) ] }, "ActivitySchema": { @@ -1666,12 +1634,20 @@ "type" ] }, - "GatewayBotResponse": { + "GatewayResponse": { "type": "object", "properties": { "url": { "type": "string" - }, + } + }, + "required": [ + "url" + ] + }, + "GatewayBotResponse": { + "type": "object", + "properties": { "shards": { "type": "integer" }, @@ -1698,22 +1674,14 @@ "reset_after", "total" ] - } - }, - "required": [ - "session_start_limit", - "shards", - "url" - ] - }, - "GatewayResponse": { - "type": "object", - "properties": { + }, "url": { "type": "string" } }, "required": [ + "session_start_limit", + "shards", "url" ] }, @@ -2276,14 +2244,8 @@ "type": "boolean" }, "channel_id": { - "anyOf": [ - { - "$ref": "#/components/schemas/Snowflake" - }, - { - "type": "null" - } - ] + "type": "string", + "nullable": true } }, "required": [ @@ -2530,6 +2492,34 @@ "location" ] }, + "PartialConnectedAccountResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "metadata": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "required": [ + "id", + "name", + "type", + "verified" + ] + }, "PreloadMessagesResponseSchema": { "type": "array", "items": { @@ -3632,6 +3622,72 @@ "type" ] }, + "CreateReportSchema": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "name": { + "type": "string" + }, + "language": { + "type": "string" + }, + "breadcrumbs": { + "type": "array", + "items": { + "type": "integer" + } + }, + "elements": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "channel_id": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "guild_id": { + "type": "string" + }, + "stage_instance_id": { + "type": "string" + }, + "guild_scheduled_event_id": { + "type": "string" + }, + "reported_user_id": { + "type": "string" + }, + "application_id": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "widget_id": { + "type": "string" + } + }, + "required": [ + "breadcrumbs", + "language", + "name", + "variant", + "version" + ] + }, "IdentifySchema": { "type": "object", "properties": { @@ -5097,34 +5153,36 @@ "user_profile" ] }, + "UserRelationResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "public_flags": { + "type": "integer" + } + }, + "required": [ + "discriminator", + "id", + "public_flags", + "username" + ] + }, "UserRelationsResponse": { "type": "array", "items": { - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "discriminator": { - "type": "string" - }, - "avatar": { - "type": "string" - }, - "public_flags": { - "type": "integer" - } - }, - "required": [ - "discriminator", - "id", - "public_flags", - "username" - ] + "$ref": "#/components/schemas/UserRelationResponse" } }, "UserRelationshipsResponse": { @@ -5369,6 +5427,37 @@ } } }, + "AckBulkSchema": { + "type": "object", + "properties": { + "read_states": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "read_state_type": { + "$ref": "#/components/schemas/ReadStateType" + } + }, + "additionalProperties": false, + "required": [ + "channel_id", + "message_id", + "read_state_type" + ] + } + } + }, + "required": [ + "read_states" + ] + }, "AcknowledgeDeleteSchema": { "type": "object", "properties": { @@ -5630,12 +5719,12 @@ "additionalProperties": true }, "original_response_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/components/schemas/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" }, "interacted_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/components/schemas/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" }, "triggering_interaction_metadata": { "$ref": "#/components/schemas/MessageInteractionSchema" @@ -5644,8 +5733,8 @@ "$ref": "#/components/schemas/PublicUser" }, "target_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/components/schemas/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" } }, "required": [ @@ -7250,72 +7339,6 @@ "name" ] }, - "CreateReportSchema": { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "name": { - "type": "string" - }, - "language": { - "type": "string" - }, - "breadcrumbs": { - "type": "array", - "items": { - "type": "integer" - } - }, - "elements": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "channel_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "guild_id": { - "type": "string" - }, - "stage_instance_id": { - "type": "string" - }, - "guild_scheduled_event_id": { - "type": "string" - }, - "reported_user_id": { - "type": "string" - }, - "application_id": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "widget_id": { - "type": "string" - } - }, - "required": [ - "breadcrumbs", - "language", - "name", - "variant", - "version" - ] - }, "SessionsLogoutSchema": { "type": "object", "properties": { @@ -13307,10 +13330,6 @@ "code" ] }, - "Snowflake": { - "description": "A container for useful snowflake-related methods.", - "type": "object" - }, "HubDirectoryEntry": { "type": "object", "properties": { diff --git a/assets/schemas.json b/assets/schemas.json index 7de8774..e28447e 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -186,33 +186,6 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, -<<<<<<< HEAD - "AckBulkSchema": { - "type": "object", - "properties": { - "read_states": { - "type": "array", - "items": { - "type": "object", - "properties": { - "channel_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "read_state_type": { - "type": "integer" - } - }, - "additionalProperties": false, - "required": [ - "channel_id", - "message_id", - "read_state_type" - ] - } -======= "RouteResponse": { "type": "object", "properties": { @@ -241,18 +214,13 @@ }, "errors": { "$ref": "#/definitions/ErrorList" ->>>>>>> 288a50d8d (Clean up imports in schemas/api/bots) } }, "additionalProperties": false, "required": [ -<<<<<<< HEAD - "read_states" -======= "code", "errors", "message" ->>>>>>> 288a50d8d (Clean up imports in schemas/api/bots) ], "$schema": "http://json-schema.org/draft-07/schema#" }, @@ -1765,12 +1733,22 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "GatewayBotResponse": { + "GatewayResponse": { "type": "object", "properties": { "url": { "type": "string" - }, + } + }, + "additionalProperties": false, + "required": [ + "url" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "GatewayBotResponse": { + "type": "object", + "properties": { "shards": { "type": "integer" }, @@ -1797,25 +1775,15 @@ "reset_after", "total" ] - } - }, - "additionalProperties": false, - "required": [ - "session_start_limit", - "shards", - "url" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "GatewayResponse": { - "type": "object", - "properties": { + }, "url": { "type": "string" } }, "additionalProperties": false, "required": [ + "session_start_limit", + "shards", "url" ], "$schema": "http://json-schema.org/draft-07/schema#" @@ -2414,13 +2382,9 @@ "type": "boolean" }, "channel_id": { - "anyOf": [ - { - "$ref": "#/definitions/Snowflake" - }, - { - "type": "null" - } + "type": [ + "null", + "string" ] } }, @@ -2685,6 +2649,36 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "PartialConnectedAccountResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "metadata": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "additionalProperties": false, + "required": [ + "id", + "name", + "type", + "verified" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "PreloadMessagesResponseSchema": { "type": "array", "items": { @@ -3852,6 +3846,74 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "CreateReportSchema": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "name": { + "type": "string" + }, + "language": { + "type": "string" + }, + "breadcrumbs": { + "type": "array", + "items": { + "type": "integer" + } + }, + "elements": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "channel_id": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "guild_id": { + "type": "string" + }, + "stage_instance_id": { + "type": "string" + }, + "guild_scheduled_event_id": { + "type": "string" + }, + "reported_user_id": { + "type": "string" + }, + "application_id": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "widget_id": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "breadcrumbs", + "language", + "name", + "variant", + "version" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "IdentifySchema": { "type": "object", "properties": { @@ -5405,34 +5467,38 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "UserRelationResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "public_flags": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "discriminator", + "id", + "public_flags", + "username" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "UserRelationsResponse": { "type": "array", "items": { - "additionalProperties": false, - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "discriminator": { - "type": "string" - }, - "avatar": { - "type": "string" - }, - "public_flags": { - "type": "integer" - } - }, - "required": [ - "discriminator", - "id", - "public_flags", - "username" - ] + "$ref": "#/definitions/UserRelationResponse" }, "$schema": "http://json-schema.org/draft-07/schema#" }, @@ -5705,6 +5771,39 @@ "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#" }, + "AckBulkSchema": { + "type": "object", + "properties": { + "read_states": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "message_id": { + "type": "string" + }, + "read_state_type": { + "$ref": "#/definitions/ReadStateType" + } + }, + "additionalProperties": false, + "required": [ + "channel_id", + "message_id", + "read_state_type" + ] + } + } + }, + "additionalProperties": false, + "required": [ + "read_states" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "AcknowledgeDeleteSchema": { "type": "object", "properties": { @@ -5972,12 +6071,12 @@ "additionalProperties": true }, "original_response_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/definitions/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" }, "interacted_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/definitions/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" }, "triggering_interaction_metadata": { "$ref": "#/definitions/MessageInteractionSchema" @@ -5986,8 +6085,8 @@ "$ref": "#/definitions/PublicUser" }, "target_message_id": { - "description": "A container for useful snowflake-related methods.", - "$ref": "#/definitions/Snowflake" + "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```", + "type": "string" } }, "additionalProperties": false, @@ -7717,74 +7816,6 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "CreateReportSchema": { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "name": { - "type": "string" - }, - "language": { - "type": "string" - }, - "breadcrumbs": { - "type": "array", - "items": { - "type": "integer" - } - }, - "elements": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "channel_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "guild_id": { - "type": "string" - }, - "stage_instance_id": { - "type": "string" - }, - "guild_scheduled_event_id": { - "type": "string" - }, - "reported_user_id": { - "type": "string" - }, - "application_id": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "widget_id": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "breadcrumbs", - "language", - "name", - "variant", - "version" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, "SessionsLogoutSchema": { "type": "object", "properties": { @@ -14212,12 +14243,6 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "Snowflake": { - "description": "A container for useful snowflake-related methods.", - "type": "object", - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - }, "HubDirectoryEntry": { "type": "object", "properties": { diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 5c40ce1..79929f5 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -17,7 +17,7 @@ */ import { route, verifyCaptcha } from "@spacebar/api"; -import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient } from "@spacebar/util"; +import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient, TimeSpan } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -26,6 +26,15 @@ const router: Router = Router({ mergeParams: true }); +const recentlyBlockedIps: { + [ip: string]: { + firstHit: Date; + lastHit: Date; + hits: number; + reason: string; + }; +} = {}; + router.post( "/", route({ @@ -82,7 +91,7 @@ throw FieldErrors({ email: { code: "DISABLED", - message: "registration is disabled on this instance", + message: "Registration is disabled on this instance", }, }); } @@ -124,20 +133,44 @@ } } + //region IP checks + const cacheBlockedIp = (ip: string, reason: string) => { + recentlyBlockedIps[ip] = { + firstHit: new Date(), + lastHit: new Date(), + hits: 0, + reason, + }; + console.log(`[Register] ${ip} blocked from registration:`, reason); + }; + + if (!regTokenUsed && recentlyBlockedIps[ip]) { + if (new TimeSpan(recentlyBlockedIps[ip].firstHit.getTime(), new Date().getTime()).totalHours >= 24) delete recentlyBlockedIps[ip]; + else { + recentlyBlockedIps[ip].lastHit = new Date(); + recentlyBlockedIps[ip].hits++; + console.log( + `[Register] ${ip} blocked from registration: blocked since ${recentlyBlockedIps[ip].firstHit} with ${recentlyBlockedIps[ip].hits} hits and reason:`, + recentlyBlockedIps[ip].reason, + ); + throw new HTTPError("Your IP is blocked from registration"); + } + } + if (!regTokenUsed && register.enableAbuseIpDb) { const blacklist = await AbuseIpDbClient.getBlacklist(); if (blacklist) { const entry = blacklist.data.find((e) => e.ipAddress === ip); if (entry && entry.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { - console.log(`[Register] ${ip} blocked from registration: AbuseIPDB score ${entry.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (BLACKLIST)`); + cacheBlockedIp(ip, `AbuseIPDB score ${entry.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (BLACKLIST)`); throw new HTTPError("Your IP is blocked from registration"); } } const checkIp = await AbuseIpDbClient.checkIpAddress(ip); if (checkIp?.data && checkIp.data.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { - console.log(`[Register] ${ip} blocked from registration: AbuseIPDB score ${checkIp.data.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (CHECK)`); + cacheBlockedIp(ip, `AbuseIPDB score ${checkIp.data.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (CHECK)`); throw new HTTPError("Your IP is blocked from registration"); } } @@ -153,25 +186,26 @@ .map(([key]) => key.replace("is_", "")); const blockedCategories = new Set(categories).intersection(new Set(register.blockIpDataCoThreatTypes)); if (blockedCategories.size > 0) { - console.log(`[Register] ${ip} blocked from registration: IPData.co threat types ${Array.from(blockedCategories).join(", ")}`); + cacheBlockedIp(ip, `IPData.co threat types ${Array.from(blockedCategories).join(", ")}`); throw new HTTPError("Your IP is blocked from registration"); } if (ipData.asn.type && register.blockAsnTypes.includes(ipData.asn.type)) { - console.log(`[Register] ${ip} blocked from registration: IPData.co ASN type ${ipData.asn.type} is blocked`); + cacheBlockedIp(ip, `IPData.co ASN type ${ipData.asn.type} is blocked`); throw new HTTPError("Your IP is blocked from registration"); } else if (!ipData.asn.type) { console.log("[Register] IPData.co response missing asn.type field", ipData); } if (ipData.asn.asn && register.blockAsns.includes(ipData.asn.asn)) { - console.log(`[Register] ${ip} blocked from registration: IPData.co ASN ${ipData.asn.name} is blocked`); + cacheBlockedIp(ip, `IPData.co ASN ${ipData.asn.name} is blocked`); throw new HTTPError("Your IP is blocked from registration"); } else if (!ipData.asn.asn) { console.log("[Register] IPData.co response missing asn.asn field", ipData); } } } + //endregion // TODO: gift_code_sku_id? // TODO: check password strength diff --git a/src/api/routes/reporting/index.ts b/src/api/routes/reporting/index.ts index 76b5169..27d284d 100644 --- a/src/api/routes/reporting/index.ts +++ b/src/api/routes/reporting/index.ts @@ -16,14 +16,13 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Request, Response, Router } from "express"; -import { ReportMenuType, ReportMenuTypeNames } from "../../../schemas/api/reports/ReportMenu"; -import path from "node:path"; -import { HTTPError } from "lambert-server"; -import { CreateReportSchema } from "../../../schemas/api/reports/CreateReport"; -import { FieldErrors } from "@spacebar/util"; import fs from "node:fs"; +import path from "node:path"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { route } from "@spacebar/api"; +import { ReportMenuType, ReportMenuTypeNames, CreateReportSchema } from "@spacebar/schemas"; +import { FieldErrors } from "@spacebar/util"; const router = Router({ mergeParams: true }); if (process.env.LOG_ROUTES !== "false") console.log("[Server] Registering reporting menu routes...");