summary refs log tree commit diff
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/assets/background.pngbin0 -> 319351 bytes
-rw-r--r--api/assets/fosscord-login.css23
-rw-r--r--api/assets/preload-plugins/loginRedirect.js13
-rw-r--r--api/assets/schemas.json2684
-rw-r--r--api/client_test/index.html8
-rw-r--r--api/locales/en/auth.json4
-rw-r--r--api/src/middlewares/Authentication.ts1
-rw-r--r--api/src/middlewares/RateLimit.ts24
-rw-r--r--api/src/routes/auth/login.ts31
-rw-r--r--api/src/routes/auth/mfa/totp.ts49
-rw-r--r--api/src/routes/auth/register.ts17
-rw-r--r--api/src/routes/auth/verify/view-backup-codes-challenge.ts26
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/ack.ts2
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts7
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts1
-rw-r--r--api/src/routes/guilds/#guild_id/member-verification.ts14
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/roles/#role_id/index.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/welcome-screen.ts (renamed from api/src/routes/guilds/#guild_id/welcome_screen.ts)4
-rw-r--r--api/src/routes/guilds/index.ts2
-rw-r--r--api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts16
-rw-r--r--api/src/routes/users/#id/profile.ts81
-rw-r--r--api/src/routes/users/@me/index.ts35
-rw-r--r--api/src/routes/users/@me/mfa/codes-verification.ts45
-rw-r--r--api/src/routes/users/@me/mfa/codes.ts48
-rw-r--r--api/src/routes/users/@me/mfa/totp/disable.ts45
-rw-r--r--api/src/routes/users/@me/mfa/totp/enable.ts53
-rw-r--r--api/src/util/handlers/Message.ts6
-rw-r--r--api/src/util/index.ts1
-rw-r--r--api/src/util/utility/captcha.ts46
30 files changed, 3205 insertions, 87 deletions
diff --git a/api/assets/background.png b/api/assets/background.png
new file mode 100644

index 00000000..58369ab8 --- /dev/null +++ b/api/assets/background.png
Binary files differdiff --git a/api/assets/fosscord-login.css b/api/assets/fosscord-login.css
index d507c545..ca0af064 100644 --- a/api/assets/fosscord-login.css +++ b/api/assets/fosscord-login.css
@@ -14,7 +14,7 @@ } h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after { margin-top: -32px; - content: "Welcome to Fosscord!"; + content: "Welcome to Slowcord!"; visibility: visible; display: block; } @@ -62,7 +62,22 @@ h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after { margin-top: -16px; } -/* shrink login box to same size as register */ -.authBoxExpanded-2jqaBe { - width: 480px !important; +/* funny styling */ +.wrapper-6URcxg { + justify-content: flex-start !important; + + background: url("/assets/background.png"); + background-size: 100% 100%; + background-repeat: no-repeat; +} + +.authBoxExpanded-2jqaBe, +.authBox-hW6HRx { + width: max(40vw, 500px) !important; + height: 100vh !important; + padding: 100px !important; + display: flex; + justify-content: center; + align-items: center; + border-radius: 0 !important; } diff --git a/api/assets/preload-plugins/loginRedirect.js b/api/assets/preload-plugins/loginRedirect.js new file mode 100644
index 00000000..895536ef --- /dev/null +++ b/api/assets/preload-plugins/loginRedirect.js
@@ -0,0 +1,13 @@ +const redirectIfOnLogin = () => { + const path = window.location.pathname; + if (path == "/login" || path == "/register") { + window.location.reload(); + } +} + +const observer = new MutationObserver((mutations) => { + redirectIfOnLogin(); +}); +observer.observe(document, { subtree: true, childList: true }) + +redirectIfOnLogin(); \ No newline at end of file diff --git a/api/assets/schemas.json b/api/assets/schemas.json
index 2102292b..4df54f40 100644 --- a/api/assets/schemas.json +++ b/api/assets/schemas.json
@@ -734,6 +734,702 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, + "hcaptchaResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "challenge_ts": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "credit": { + "type": "boolean" + }, + "error-codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "score": { + "type": "integer" + }, + "score_reason": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "challenge_ts", + "credit", + "error-codes", + "hostname", + "score", + "score_reason", + "success" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "recaptchaResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "score": { + "type": "integer" + }, + "action": { + "type": "string" + }, + "challenge_ts": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "error-codes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "action", + "challenge_ts", + "hostname", + "score", + "success" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, "LoginSchema": { "type": "object", "properties": { @@ -1072,6 +1768,344 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, + "TotpSchema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "ticket": { + "type": "string" + }, + "gift_code_sku_id": { + "type": [ + "null", + "string" + ] + }, + "login_source": { + "type": [ + "null", + "string" + ] + } + }, + "additionalProperties": false, + "required": [ + "code", + "ticket" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, "RegisterSchema": { "type": "object", "properties": { @@ -1106,6 +2140,9 @@ }, "captcha_key": { "type": "string" + }, + "promotional_email_opt_in": { + "type": "boolean" } }, "additionalProperties": false, @@ -1424,6 +2461,328 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, + "BackupCodesChallengeSchema": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "password" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, "ChannelModifySchema": { "type": "object", "properties": { @@ -6511,9 +7870,6 @@ } }, "additionalProperties": false, - "required": [ - "name" - ], "definitions": { "Embed": { "type": "object", @@ -6873,6 +8229,9 @@ "preferred_locale": { "type": "string" }, + "premium_progress_bar_enabled": { + "type": "boolean" + }, "name": { "maxLength": 100, "type": "string" @@ -6897,9 +8256,6 @@ } }, "additionalProperties": false, - "required": [ - "name" - ], "definitions": { "Embed": { "type": "object", @@ -7219,6 +8575,9 @@ "items": { "type": "string" } + }, + "nick": { + "type": "string" } }, "additionalProperties": false, @@ -10515,8 +11874,7 @@ "additionalProperties": false, "required": [ "channel_id", - "description", - "emoji_name" + "description" ] } }, @@ -12530,9 +13888,1319 @@ }, "code": { "type": "string" + }, + "email": { + "type": "string" + }, + "discriminator": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "CodesVerificationSchema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "nonce": { + "type": "string" + }, + "regenerate": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "key", + "nonce" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "MfaCodesSchema": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "regenerate": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "password" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "TotpDisableSchema": { + "type": "object", + "properties": { + "code": { + "type": "string" } }, "additionalProperties": false, + "required": [ + "code" + ], + "definitions": { + "Embed": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "value" + ] + } + } + }, + "additionalProperties": false + }, + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Record<string,string>": { + "type": "object", + "additionalProperties": false + }, + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "number" + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 14, + 15, + 2, + 255, + 3, + 33, + 34, + 35, + 4, + 5, + 6, + 64, + 7, + 8, + 9 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + }, + "premium_since": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "premium_since", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "name", + "type", + "verified" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "TotpEnableSchema": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "code": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "password" + ], "definitions": { "Embed": { "type": "object", diff --git a/api/client_test/index.html b/api/client_test/index.html
index b438b492..7a3e4695 100644 --- a/api/client_test/index.html +++ b/api/client_test/index.html
@@ -71,10 +71,10 @@ } </script> <script src="/assets/checkLocale.js"></script> - <script src="/assets/1e18f2aac02e172db283.js"></script> - <script src="/assets/681e53cdfefa5b82249a.js"></script> - <script src="/assets/7a036838c0a0e73f59d8.js"></script> - <script src="/assets/b6cf2184a7a05e7525ce.js"></script> + <script src="/assets/83ace7450e110d16319e.js"></script> + <script src="/assets/e02290aaa8dac5d195c2.js"></script> + <script src="/assets/4f3b3c576b879a5f75d1.js"></script> + <script src="/assets/699456246fdfe7589855.js"></script> <!-- plugin marker --> </body> </html> diff --git a/api/locales/en/auth.json b/api/locales/en/auth.json
index e19547a0..a78d4d60 100644 --- a/api/locales/en/auth.json +++ b/api/locales/en/auth.json
@@ -2,7 +2,9 @@ "login": { "INVALID_LOGIN": "E-Mail or Phone not found", "INVALID_PASSWORD": "Invalid Password", - "ACCOUNT_DISABLED": "This account is disabled" + "ACCOUNT_DISABLED": "This account is disabled", + "INVALID_TOTP_CODE": "Invalid two-factor code.", + "INVALID_TOTP_SECRET": "Invalid two-factor secret." }, "register": { "REGISTRATION_DISABLED": "New user registration is disabled", diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts
index 5a08caf3..1df7911b 100644 --- a/api/src/middlewares/Authentication.ts +++ b/api/src/middlewares/Authentication.ts
@@ -7,6 +7,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "/auth/login", "/auth/register", "/auth/location-metadata", + "/auth/mfa/totp", // Routes with a seperate auth system "/webhooks/", // Public information endpoints diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index 13f1602c..1a38cfcf 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts
@@ -1,4 +1,4 @@ -import { Config, getRights, listenEvent, Rights } from "@fosscord/util"; +import { Config, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { getIpAdress } from "@fosscord/api"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; @@ -9,7 +9,6 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; /* ? bucket limit? Max actions/sec per bucket? -(ANSWER: a small fosscord instance might not need a complex rate limiting system) TODO: delay database requests to include multiple queries TODO: different for methods (GET/POST) @@ -45,12 +44,6 @@ export default function rateLimit(opts: { onlyIp?: boolean; }): any { return async (req: Request, res: Response, next: NextFunction): Promise<any> => { - // exempt user? if so, immediately short circuit - if (req.user_id) { - const rights = await getRights(req.user_id); - if (rights.has("BYPASS_RATE_LIMITS")) return; - } - const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var executor_id = getIpAdress(req); if (!opts.onlyIp && req.user_id) executor_id = req.user_id; @@ -60,12 +53,12 @@ export default function rateLimit(opts: { if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; - let offender = Cache.get(executor_id + bucket_id); + const offender = Cache.get(executor_id + bucket_id); if (offender) { - let reset = offender.expires_at.getTime(); - let resetAfterMs = reset - Date.now(); - let resetAfterSec = Math.ceil(resetAfterMs / 1000); + const reset = offender.expires_at.getTime(); + const resetAfterMs = reset - Date.now(); + const resetAfterSec = resetAfterMs / 1000; if (resetAfterMs <= 0) { offender.hits = 0; @@ -77,11 +70,6 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; - // each block violation pushes the expiry one full window further - reset += opts.window * 1000; - offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); - resetAfterMs = reset - Date.now(); - resetAfterSec = Math.ceil(resetAfterMs / 1000); console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( @@ -163,7 +151,7 @@ export async function initRateLimits(app: Router) { app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) { +async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { const id = opts.executor_id + opts.bucket_id; var limit = Cache.get(id); if (!limit) { diff --git a/api/src/routes/auth/login.ts b/api/src/routes/auth/login.ts
index a89721ea..bcaccb30 100644 --- a/api/src/routes/auth/login.ts +++ b/api/src/routes/auth/login.ts
@@ -1,7 +1,8 @@ import { Request, Response, Router } from "express"; -import { route } from "@fosscord/api"; +import { route, getIpAdress, verifyCaptcha } from "@fosscord/api"; import bcrypt from "bcrypt"; import { Config, User, generateToken, adjustEmail, FieldErrors } from "@fosscord/util"; +import crypto from "crypto"; const router: Router = Router(); export default router; @@ -23,8 +24,8 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo const config = Config.get(); if (config.login.requireCaptcha && config.security.captcha.enabled) { + const { sitekey, service } = config.security.captcha; if (!captcha_key) { - const { sitekey, service } = config.security.captcha; return res.status(400).json({ captcha_key: ["captcha-required"], captcha_sitekey: sitekey, @@ -32,12 +33,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo }); } - // TODO: check captcha + const ip = getIpAdress(req); + const verify = await verifyCaptcha(captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service + }); + } } const user = await User.findOneOrFail({ where: [{ phone: login }, { email: login }], - select: ["data", "id", "disabled", "deleted", "settings"] + select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"] }).catch((e) => { throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); }); @@ -57,6 +66,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); } + if (user.mfa_enabled) { + // TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy + const ticket = crypto.randomBytes(40).toString("hex"); + + await User.update({ id: user.id }, { totp_last_ticket: ticket }); + + return res.json({ + ticket: ticket, + mfa: true, + sms: false, // TODO + token: null, + }) + } + const token = await generateToken(user.id); // Notice this will have a different token structure, than discord diff --git a/api/src/routes/auth/mfa/totp.ts b/api/src/routes/auth/mfa/totp.ts new file mode 100644
index 00000000..cec6e5ee --- /dev/null +++ b/api/src/routes/auth/mfa/totp.ts
@@ -0,0 +1,49 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { BackupCode, FieldErrors, generateToken, User } from "@fosscord/util"; +import { verifyToken } from "node-2fa"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +export interface TotpSchema { + code: string, + ticket: string, + gift_code_sku_id?: string | null, + login_source?: string | null, +} + +router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => { + const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema; + + const user = await User.findOneOrFail({ + where: { + totp_last_ticket: ticket, + }, + select: [ + "id", + "totp_secret", + "settings", + ], + }); + + const backup = await BackupCode.findOne({ code: code, expired: false, consumed: false, user: { id: user.id }}); + + if (!backup) { + const ret = verifyToken(user.totp_secret!, code); + if (!ret || ret.delta != 0) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } + else { + backup.consumed = true; + await backup.save(); + } + + await User.update({ id: user.id }, { totp_last_ticket: "" }); + + return res.json({ + token: await generateToken(user.id), + user_settings: user.settings, + }); +}); + +export default router; diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts
index 94dd6502..f74d0d63 100644 --- a/api/src/routes/auth/register.ts +++ b/api/src/routes/auth/register.ts
@@ -1,6 +1,6 @@ import { Request, Response, Router } from "express"; -import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, trimSpecial } from "@fosscord/util"; -import { route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; +import { Config, generateToken, Invite, FieldErrors, User, adjustEmail } from "@fosscord/util"; +import { route, getIpAdress, IPAnalysis, isProxy, verifyCaptcha } from "@fosscord/api"; import "missing-native-js-functions"; import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; @@ -31,6 +31,8 @@ export interface RegisterSchema { date_of_birth?: Date; // "2000-04-03" gift_code_sku_id?: string; captcha_key?: string; + + promotional_email_opt_in?: boolean; } router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { @@ -65,8 +67,8 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } if (register.requireCaptcha && security.captcha.enabled) { + const { sitekey, service } = security.captcha; if (!body.captcha_key) { - const { sitekey, service } = security.captcha; return res?.status(400).json({ captcha_key: ["captcha-required"], captcha_sitekey: sitekey, @@ -74,7 +76,14 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - // TODO: check captcha + const verify = await verifyCaptcha(body.captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service + }); + } } if (!register.allowMultipleAccounts) { diff --git a/api/src/routes/auth/verify/view-backup-codes-challenge.ts b/api/src/routes/auth/verify/view-backup-codes-challenge.ts new file mode 100644
index 00000000..be651686 --- /dev/null +++ b/api/src/routes/auth/verify/view-backup-codes-challenge.ts
@@ -0,0 +1,26 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { FieldErrors, User } from "@fosscord/util"; +import bcrypt from "bcrypt"; +const router = Router(); + +export interface BackupCodesChallengeSchema { + password: string; +} + +router.post("/", route({ body: "BackupCodesChallengeSchema" }), async (req: Request, res: Response) => { + const { password } = req.body as BackupCodesChallengeSchema; + + const user = await User.findOneOrFail({ id: req.user_id }, { select: ["data"] }); + + if (!await bcrypt.compare(password, user.data.hash || "")) { + throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + + return res.json({ + nonce: "NoncePlaceholder", + regenerate_nonce: "RegenNoncePlaceholder", + }) +}); + +export default router; diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
index 885c5eca..1e3564d8 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -35,7 +35,7 @@ router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Reques } } as MessageAckEvent); - res.sendStatus(204); + res.json({ token: null }); }); export default router; diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index 54e6edcc..00e38239 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -102,12 +102,11 @@ router.get("/", async (req: Request, res: Response) => { if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); var query: FindManyOptions<Message> & { where: { id?: any; }; } = { - order: { id: "DESC" }, + order: { timestamp: "DESC" }, take: limit, where: { channel_id }, relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] }; - if (after) { if (after > new Snowflake()) return res.status(422); @@ -179,7 +178,7 @@ const messageUpload = multer({ router.post( "/", messageUpload.any(), - async (req, res, next) => { + (req, res, next) => { if (req.body.payload_json) { req.body = JSON.parse(req.body.payload_json); } @@ -228,7 +227,7 @@ router.post( const channel_dto = await DmChannelDTO.from(channel); // Only one recipients should be closed here, since in group DMs the recipient is deleted not closed - Promise.all( + await Promise.all( channel.recipients!.map((recipient) => { if (recipient.closed) { recipient.closed = false; diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 4ec3df72..45e30a74 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -20,6 +20,7 @@ export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { afk_timeout?: number; afk_channel_id?: string; preferred_locale?: string; + premium_progress_bar_enabled?: boolean; } router.get("/", route({}), async (req: Request, res: Response) => { diff --git a/api/src/routes/guilds/#guild_id/member-verification.ts b/api/src/routes/guilds/#guild_id/member-verification.ts new file mode 100644
index 00000000..265a1b35 --- /dev/null +++ b/api/src/routes/guilds/#guild_id/member-verification.ts
@@ -0,0 +1,14 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +const router = Router(); + +router.get("/",route({}), async (req: Request, res: Response) => { + // TODO: member verification + + res.status(404).json({ + message: "Unknown Guild Member Verification Form", + code: 10068 + }); +}); + +export default router; diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index c285abb3..2ff89eae 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -7,6 +7,7 @@ const router = Router(); export interface MemberChangeSchema { roles?: string[]; + nick?: string; } router.get("/", route({}), async (req: Request, res: Response) => { @@ -34,6 +35,8 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re member.roles = body.roles.map((x) => new Role({ id: x })); // foreign key constraint will fail if role doesn't exist } + if (body.nick) member.nick = body.nick; + await member.save(); member.roles = member.roles.filter((x) => x.id !== everyone.id); diff --git a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
index 2ad01682..16b5a59f 100644 --- a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts +++ b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -41,7 +41,8 @@ router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" } const { role_id, guild_id } = req.params; const body = req.body as RoleModifySchema; - if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); + if (body.icon && body.icon.length) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); + else body.icon = undefined; const role = new Role({ ...body, diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome-screen.ts
index 7141f17e..5c7a9daa 100644 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ b/api/src/routes/guilds/#guild_id/welcome-screen.ts
@@ -10,7 +10,7 @@ export interface GuildUpdateWelcomeScreenSchema { channel_id: string; description: string; emoji_id?: string; - emoji_name: string; + emoji_name?: string; }[]; enabled?: boolean; description?: string; @@ -36,6 +36,8 @@ router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "M if (body.description) guild.welcome_screen.description = body.description; if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; + await guild.save(); + res.sendStatus(204); }); diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index 10721413..489dea49 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts
@@ -9,7 +9,7 @@ export interface GuildCreateSchema { /** * @maxLength 100 */ - name: string; + name?: string; region?: string; icon?: string | null; channels?: ChannelModifySchema[]; diff --git a/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
index 723a5160..03162ec8 100644 --- a/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts +++ b/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
@@ -5,6 +5,22 @@ const router: Router = Router(); const skus = new Map([ [ + "978380684370378762", + [ + { + id: "978380692553465866", + name: "Nitro Lite Monthly", + interval: 1, + interval_count: 1, + tag_inclusive: true, + sku_id: "978380684370378762", + currency: "usd", + price: 0, + price_tier: null, + } + ] + ], + [ "521842865731534868", [ { diff --git a/api/src/routes/users/#id/profile.ts b/api/src/routes/users/#id/profile.ts
index 4dbb84cf..a77fbdb5 100644 --- a/api/src/routes/users/#id/profile.ts +++ b/api/src/routes/users/#id/profile.ts
@@ -1,5 +1,5 @@ import { Router, Request, Response } from "express"; -import { PublicConnectedAccount, PublicUser, User, UserPublic, Member } from "@fosscord/util"; +import { PublicConnectedAccount, PublicUser, User, UserPublic, Member, Guild } from "@fosscord/util"; import { route } from "@fosscord/api"; const router: Router = Router(); @@ -13,45 +13,78 @@ export interface UserProfileResponse { router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { if (req.params.id === "@me") req.params.id = req.user_id; + + const { guild_id, with_mutual_guilds } = req.query; + const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); var mutual_guilds: object[] = []; var premium_guild_since; - const requested_member = await Member.find( { id: req.params.id, }) - const self_member = await Member.find( { id: req.user_id, }) - for(const rmem of requested_member) { - if(rmem.premium_since) { - if(premium_guild_since){ - if(premium_guild_since > rmem.premium_since) { + if (with_mutual_guilds == "true") { + const requested_member = await Member.find({ id: req.params.id, }); + const self_member = await Member.find({ id: req.user_id, }); + + for (const rmem of requested_member) { + if (rmem.premium_since) { + if (premium_guild_since) { + if (premium_guild_since > rmem.premium_since) { + premium_guild_since = rmem.premium_since; + } + } else { premium_guild_since = rmem.premium_since; } - } else { - premium_guild_since = rmem.premium_since; } - } - for(const smem of self_member) { - if (smem.guild_id === rmem.guild_id) { - mutual_guilds.push({id: rmem.guild_id, nick: rmem.nick}) + for (const smem of self_member) { + if (smem.guild_id === rmem.guild_id) { + mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick }); + } } } } + + const guild_member = guild_id && typeof guild_id == "string" + ? await Member.findOneOrFail({ id: req.params.id, guild_id: guild_id }, { relations: ["roles"] }) + : undefined; + + // TODO: make proper DTO's in util? + + const userDto = { + username: user.username, + discriminator: user.discriminator, + id: user.id, + public_flags: user.public_flags, + avatar: user.avatar, + accent_color: user.accent_color, + banner: user.banner, + bio: req.user_bot ? null : user.bio, + bot: user.bot + }; + + const guildMemberDto = guild_member ? { + avatar: user.avatar, // TODO + banner: user.banner, // TODO + bio: req.user_bot ? null : user.bio, // TODO + communication_disabled_until: null, // TODO + deaf: guild_member.deaf, + flags: user.flags, + is_pending: guild_member.pending, + pending: guild_member.pending, // why is this here twice, discord? + joined_at: guild_member.joined_at, + mute: guild_member.mute, + nick: guild_member.nick, + premium_since: guild_member.premium_since, + roles: guild_member.roles.map(x => x.id).filter(id => id != guild_id), + user: userDto + } : undefined; + res.json({ connected_accounts: user.connected_accounts, premium_guild_since: premium_guild_since, // TODO premium_since: user.premium_since, // TODO mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true - user: { - username: user.username, - discriminator: user.discriminator, - id: user.id, - public_flags: user.public_flags, - avatar: user.avatar, - accent_color: user.accent_color, - banner: user.banner, - bio: req.user_bot ? null : user.bio, - bot: user.bot - } + user: userDto, + guild_member: guildMemberDto, }); }); diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index 1af413c4..dc0d1cb1 100644 --- a/api/src/routes/users/@me/index.ts +++ b/api/src/routes/users/@me/index.ts
@@ -1,7 +1,8 @@ import { Router, Request, Response } from "express"; -import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors } from "@fosscord/util"; +import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors, adjustEmail, Config } from "@fosscord/util"; import { route } from "@fosscord/api"; import bcrypt from "bcrypt"; +import { HTTPError } from "lambert-server"; const router: Router = Router(); @@ -21,6 +22,8 @@ export interface UserModifySchema { password?: string; new_password?: string; code?: string; + email?: string; + discriminator?: string; } router.get("/", route({}), async (req: Request, res: Response) => { @@ -30,11 +33,13 @@ router.get("/", route({}), async (req: Request, res: Response) => { router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); + + if (user.email == "demo@maddy.k.vu") throw new HTTPError("Demo user, sorry", 400); + if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string); if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); - if (body.password) { if (user.data?.hash) { const same_password = await bcrypt.compare(body.password, user.data.hash || ""); @@ -46,6 +51,14 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: } } + if (body.email) { + body.email = adjustEmail(body.email); + if (!body.email && Config.get().register.email.required) + throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } }); + if (!body.password) + throw FieldErrors({ password: { message: req.t("auth:register.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + if (body.new_password) { if (!body.password && !user.email) { throw FieldErrors({ @@ -55,14 +68,14 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: user.data.hash = await bcrypt.hash(body.new_password, 12); } - if(body.username){ - var check_username = body?.username?.replace(/\s/g, ''); - if(!check_username) { - throw FieldErrors({ - username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } - } + if (body.username) { + var check_username = body?.username?.replace(/\s/g, ''); + if (!check_username) { + throw FieldErrors({ + username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + }); + } + } user.assign(body); await user.save(); diff --git a/api/src/routes/users/@me/mfa/codes-verification.ts b/api/src/routes/users/@me/mfa/codes-verification.ts new file mode 100644
index 00000000..3aca44a6 --- /dev/null +++ b/api/src/routes/users/@me/mfa/codes-verification.ts
@@ -0,0 +1,45 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { BackupCode, generateMfaBackupCodes, User } from "@fosscord/util"; + +const router = Router(); + +export interface CodesVerificationSchema { + key: string; + nonce: string; + regenerate?: boolean; +} + +router.post("/", route({ body: "CodesVerificationSchema" }), async (req: Request, res: Response) => { + const { key, nonce, regenerate } = req.body as CodesVerificationSchema; + + // TODO: We don't have email/etc etc, so can't send a verification code. + // Once that's done, this route can verify `key` + + const user = await User.findOneOrFail({ id: req.user_id }); + + var codes: BackupCode[]; + if (regenerate) { + await BackupCode.update( + { user: { id: req.user_id } }, + { expired: true } + ); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map(x => x.save())); + } + else { + codes = await BackupCode.find({ + user: { + id: req.user_id, + }, + expired: false, + }); + } + + return res.json({ + backup_codes: codes.map(x => ({ ...x, expired: undefined })), + }) +}); + +export default router; diff --git a/api/src/routes/users/@me/mfa/codes.ts b/api/src/routes/users/@me/mfa/codes.ts new file mode 100644
index 00000000..2a1fb498 --- /dev/null +++ b/api/src/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,48 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { BackupCode, FieldErrors, generateMfaBackupCodes, User } from "@fosscord/util"; +import bcrypt from "bcrypt"; + +const router = Router(); + +export interface MfaCodesSchema { + password: string; + regenerate?: boolean; +} + +// TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients + +router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => { + const { password, regenerate } = req.body as MfaCodesSchema; + + const user = await User.findOneOrFail({ id: req.user_id }, { select: ["data"] }); + + if (!await bcrypt.compare(password, user.data.hash || "")) { + throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + + var codes: BackupCode[]; + if (regenerate) { + await BackupCode.update( + { user: { id: req.user_id } }, + { expired: true } + ); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map(x => x.save())); + } + else { + codes = await BackupCode.find({ + user: { + id: req.user_id, + }, + expired: false, + }); + } + + return res.json({ + backup_codes: codes.map(x => ({ ...x, expired: undefined })), + }) +}); + +export default router; diff --git a/api/src/routes/users/@me/mfa/totp/disable.ts b/api/src/routes/users/@me/mfa/totp/disable.ts new file mode 100644
index 00000000..5e039ea3 --- /dev/null +++ b/api/src/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,45 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { verifyToken } from 'node-2fa'; +import { HTTPError } from "lambert-server"; +import { User, generateToken, BackupCode } from "@fosscord/util"; + +const router = Router(); + +export interface TotpDisableSchema { + code: string; +} + +router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => { + const body = req.body as TotpDisableSchema; + + const user = await User.findOneOrFail({ id: req.user_id }, { select: ["totp_secret"] }); + + const backup = await BackupCode.findOne({ code: body.code }); + if (!backup) { + const ret = verifyToken(user.totp_secret!, body.code); + if (!ret || ret.delta != 0) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } + + await User.update( + { id: req.user_id }, + { + mfa_enabled: false, + totp_secret: "", + }, + ); + + await BackupCode.update( + { user: { id: req.user_id } }, + { + expired: true, + } + ); + + return res.json({ + token: await generateToken(user.id), + }); +}); + +export default router; \ No newline at end of file diff --git a/api/src/routes/users/@me/mfa/totp/enable.ts b/api/src/routes/users/@me/mfa/totp/enable.ts new file mode 100644
index 00000000..e4ce9ce0 --- /dev/null +++ b/api/src/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,53 @@ +import { Router, Request, Response } from "express"; +import { User, generateToken, BackupCode, generateMfaBackupCodes } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import bcrypt from "bcrypt"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from 'node-2fa'; +import crypto from "crypto"; + +const router = Router(); + +export interface TotpEnableSchema { + password: string; + code?: string; + secret?: string; +} + +router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => { + const body = req.body as TotpEnableSchema; + + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data", "email"] }); + + if (user.email == "demo@maddy.k.vu") throw new HTTPError("Demo user, sorry", 400); + + // TODO: Are guests allowed to enable 2fa? + if (user.data.hash) { + if (!await bcrypt.compare(body.password, user.data.hash)) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } + } + + if (!body.secret) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); + + if (!body.code) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + if (verifyToken(body.secret, body.code)?.delta != 0) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + let backup_codes = generateMfaBackupCodes(req.user_id); + await Promise.all(backup_codes.map(x => x.save())); + await User.update( + { id: req.user_id }, + { mfa_enabled: true, totp_secret: body.secret } + ); + + res.send({ + token: await generateToken(user.id), + backup_codes: backup_codes.map(x => ({ ...x, expired: undefined })), + }); +}); + +export default router; \ No newline at end of file diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts
index 48f87dfe..ed17995b 100644 --- a/api/src/util/handlers/Message.ts +++ b/api/src/util/handlers/Message.ts
@@ -166,7 +166,11 @@ export async function postHandleMessage(message: Message) { const provider_name = $('meta[property="og:site_name"]').text(); const author_name = $('meta[property="article:author"]').attr("content"); const description = $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content"); + const image = $('meta[property="og:image"]').attr("content"); + const width = parseInt($('meta[property="og:image:width"]').attr("content") || "") || undefined; + const height = parseInt($('meta[property="og:image:height"]').attr("content") || "") || undefined; + const url = $('meta[property="og:url"]').attr("content"); // TODO: color const embed: Embed = { @@ -177,7 +181,7 @@ export async function postHandleMessage(message: Message) { }; if (author_name) embed.author = { name: author_name }; - if (image) embed.thumbnail = { proxy_url: image, url: image }; + if (image) embed.thumbnail = { proxy_url: image, url: image, width: width, height: height }; if (title) embed.title = title; if (url) embed.url = url; if (description) embed.description = description; diff --git a/api/src/util/index.ts b/api/src/util/index.ts
index ffbcf24e..de6b6064 100644 --- a/api/src/util/index.ts +++ b/api/src/util/index.ts
@@ -6,3 +6,4 @@ export * from "./utility/RandomInviteID"; export * from "./handlers/route"; export * from "./utility/String"; export * from "./handlers/Voice"; +export * from "./utility/captcha"; \ No newline at end of file diff --git a/api/src/util/utility/captcha.ts b/api/src/util/utility/captcha.ts new file mode 100644
index 00000000..739647d2 --- /dev/null +++ b/api/src/util/utility/captcha.ts
@@ -0,0 +1,46 @@ +import { Config } from "@fosscord/util"; +import fetch from "node-fetch"; + +export interface hcaptchaResponse { + success: boolean; + challenge_ts: string; + hostname: string; + credit: boolean; + "error-codes": string[]; + score: number; // enterprise only + score_reason: string[]; // enterprise only +} + +export interface recaptchaResponse { + success: boolean; + score: number; // between 0 - 1 + action: string; + challenge_ts: string; + hostname: string; + "error-codes"?: string[]; +} + +const verifyEndpoints = { + hcaptcha: "https://hcaptcha.com/siteverify", + recaptcha: "https://www.google.com/recaptcha/api/siteverify", +} + +export async function verifyCaptcha(response: string, ip?: string) { + const { security } = Config.get(); + const { service, secret, sitekey } = security.captcha; + + if (!service) throw new Error("Cannot verify captcha without service"); + + const res = await fetch(verifyEndpoints[service], { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `response=${encodeURIComponent(response)}` + + `&secret=${encodeURIComponent(secret!)}` + + `&sitekey=${encodeURIComponent(sitekey!)}` + + (ip ? `&remoteip=${encodeURIComponent(ip!)}` : ""), + }); + + return await res.json() as hcaptchaResponse | recaptchaResponse; +} \ No newline at end of file