From 5ad3a906f6c0d9b01dc1cc018ef5a07123d28cff Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 10:48:47 -0500 Subject: [PATCH 01/18] lexicons --- lexicons/com/atproto/server/confirmEmail.json | 22 ++++++++++++++++ .../server/requestEmailConfirmation.json | 10 ++++++++ .../atproto/server/requestEmailUpdate.json | 10 ++++++++ lexicons/com/atproto/server/updateEmail.json | 25 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 lexicons/com/atproto/server/confirmEmail.json create mode 100644 lexicons/com/atproto/server/requestEmailConfirmation.json create mode 100644 lexicons/com/atproto/server/requestEmailUpdate.json create mode 100644 lexicons/com/atproto/server/updateEmail.json diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json new file mode 100644 index 00000000000..ad064cf64b9 --- /dev/null +++ b/lexicons/com/atproto/server/confirmEmail.json @@ -0,0 +1,22 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.confirmEmail", + "defs": { + "main": { + "type": "procedure", + "description": "Confirm an email using a token from com.atproto.server.requestEmailConfirmation.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["email", "token"], + "properties": { + "email": { "type": "string" }, + "token": { "type": "string" } + } + } + }, + "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] + } + } +} diff --git a/lexicons/com/atproto/server/requestEmailConfirmation.json b/lexicons/com/atproto/server/requestEmailConfirmation.json new file mode 100644 index 00000000000..4b2470bf59b --- /dev/null +++ b/lexicons/com/atproto/server/requestEmailConfirmation.json @@ -0,0 +1,10 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.requestEmailConfirmation", + "defs": { + "main": { + "type": "procedure", + "description": "Request an email with a code to confirm ownership of email" + } + } +} diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json new file mode 100644 index 00000000000..37c435d72e5 --- /dev/null +++ b/lexicons/com/atproto/server/requestEmailUpdate.json @@ -0,0 +1,10 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.requestEmailUpdate", + "defs": { + "main": { + "type": "procedure", + "description": "Request a token in order to update email." + } + } +} diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json new file mode 100644 index 00000000000..2cd84a3e5bc --- /dev/null +++ b/lexicons/com/atproto/server/updateEmail.json @@ -0,0 +1,25 @@ +{ + "lexicon": 1, + "id": "com.atproto.server.updateEmail", + "defs": { + "main": { + "type": "procedure", + "description": "Update an account's email.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["email"], + "properties": { + "email": { "type": "string" }, + "token": { + "type": "string", + "description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed." + } + } + } + }, + "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] + } + } +} From f97e3abdad98e6672ddb6f0d2eb02c41da142cd6 Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 10:49:50 -0500 Subject: [PATCH 02/18] codegen --- packages/api/docs/labels.md | 54 +- .../api/docs/moderation-behaviors/posts.md | 529 +++++++++++++++++- .../api/docs/moderation-behaviors/profiles.md | 188 ++++++- packages/api/src/client/index.ts | 52 ++ packages/api/src/client/lexicons.ts | 95 ++++ .../types/com/atproto/server/confirmEmail.ts | 47 ++ .../server/requestEmailConfirmation.ts | 28 + .../com/atproto/server/requestEmailUpdate.ts | 28 + .../types/com/atproto/server/updateEmail.ts | 48 ++ packages/bsky/src/lexicon/index.ts | 48 ++ packages/bsky/src/lexicon/lexicons.ts | 95 ++++ .../types/com/atproto/server/confirmEmail.ts | 40 ++ .../server/requestEmailConfirmation.ts | 31 + .../com/atproto/server/requestEmailUpdate.ts | 31 + .../types/com/atproto/server/updateEmail.ts | 41 ++ packages/pds/src/lexicon/index.ts | 48 ++ packages/pds/src/lexicon/lexicons.ts | 95 ++++ .../types/com/atproto/server/confirmEmail.ts | 40 ++ .../server/requestEmailConfirmation.ts | 31 + .../com/atproto/server/requestEmailUpdate.ts | 31 + .../types/com/atproto/server/updateEmail.ts | 41 ++ 21 files changed, 1587 insertions(+), 54 deletions(-) create mode 100644 packages/api/src/client/types/com/atproto/server/confirmEmail.ts create mode 100644 packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts create mode 100644 packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts create mode 100644 packages/api/src/client/types/com/atproto/server/updateEmail.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md index a2d8806b566..9531df460c1 100644 --- a/packages/api/docs/labels.md +++ b/packages/api/docs/labels.md @@ -1,44 +1,44 @@ -# Labels + # Labels + + This document is a reference for the labels used in the SDK. -This document is a reference for the labels used in the SDK. + **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. -**⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. + ## Key -## Key + ### Label Preferences -### Label Preferences + The possible client interpretations for a label. -The possible client interpretations for a label. + - ignore Do nothing with the label. + - warn Provide some form of warning on the content (see "On Warn" behavior). + - hide Remove the content from feeds and apply the warning when directly viewed. -- ignore Do nothing with the label. -- warn Provide some form of warning on the content (see "On Warn" behavior). -- hide Remove the content from feeds and apply the warning when directly viewed. + Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. -Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. + ### Configurable? -### Configurable? + Non-configurable labels cannot have their preference changed by the user. -Non-configurable labels cannot have their preference changed by the user. + ### Flags -### Flags + Additional behaviors which a label can adopt. -Additional behaviors which a label can adopt. + - no-override The user cannot click through any covering of content created by the label. + - adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. -- no-override The user cannot click through any covering of content created by the label. -- adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. + ### On Warn -### On Warn + The kind of UI behavior used when a warning must be applied. -The kind of UI behavior used when a warning must be applied. + - blur Hide all of the content behind an interstitial. + - blur-media Hide only the media within the content (ie images) behind an interstitial. + - alert Display a descriptive warning but do not hide the content. + - null Do nothing. -- blur Hide all of the content behind an interstitial. -- blur-media Hide only the media within the content (ie images) behind an interstitial. -- alert Display a descriptive warning but do not hide the content. -- null Do nothing. - -## Label Behaviors + ## Label Behaviors @@ -267,7 +267,7 @@ The kind of UI behavior used when a warning must be applied.
-## Label Group Descriptions + ## Label Group Descriptions @@ -312,7 +312,7 @@ The kind of UI behavior used when a warning must be applied.
-## Label Descriptions + ## Label Descriptions @@ -535,4 +535,4 @@ The kind of UI behavior used when a warning must be applied. on content
Misleading
The moderators believe this account is spreading misleading information.

-
+ \ No newline at end of file diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md index 5ddcf9ff602..ef3c6c7fc5d 100644 --- a/packages/api/docs/moderation-behaviors/posts.md +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -38,12 +38,17 @@ Key: + + + + + Imperative label ('!hide') on author profile @@ -51,6 +56,7 @@ Key: + 🚫 @@ -58,9 +64,13 @@ Key: + + + + Imperative label ('!hide') on author account @@ -76,9 +86,13 @@ Key: + + + + Imperative label ('!hide') on quoted post @@ -86,9 +100,11 @@ Key: + + 🚫 @@ -96,6 +112,9 @@ Key: + + + Imperative label ('!hide') on quoted author account @@ -103,9 +122,11 @@ Key: + + 🚫 @@ -113,6 +134,9 @@ Key: + + + Imperative label ('!no-promote') on post @@ -120,15 +144,21 @@ Key: + + + + + + Imperative label ('!no-promote') on author profile @@ -136,15 +166,21 @@ Key: + + + + + + Imperative label ('!no-promote') on author account @@ -152,15 +188,21 @@ Key: + + + + + + Imperative label ('!no-promote') on quoted post @@ -168,15 +210,21 @@ Key: + + + + + + Imperative label ('!no-promote') on quoted author account @@ -184,15 +232,21 @@ Key: + + + + + + Imperative label ('!warn') on post @@ -204,12 +258,17 @@ Key: + + + + + Imperative label ('!warn') on author profile @@ -217,6 +276,7 @@ Key: + ✋ @@ -224,9 +284,13 @@ Key: + + + + Imperative label ('!warn') on author account @@ -242,9 +306,13 @@ Key: + + + + Imperative label ('!warn') on quoted post @@ -252,9 +320,11 @@ Key: + + ✋ @@ -262,6 +332,9 @@ Key: + + + Imperative label ('!warn') on quoted author account @@ -269,9 +342,11 @@ Key: + + ✋ @@ -279,6 +354,8 @@ Key: + + ScenarioFilterContentAvatarEmbed Blur label ('intolerant') on post (hide) @@ -291,12 +368,17 @@ Key: + + + + + Blur label ('intolerant') on author profile (hide) @@ -304,6 +386,7 @@ Key: + ✋ @@ -311,9 +394,13 @@ Key: + + + + Blur label ('intolerant') on author account (hide) @@ -329,9 +416,13 @@ Key: + + + + Blur label ('intolerant') on quoted post (hide) @@ -339,9 +430,11 @@ Key: + + ✋ @@ -349,6 +442,9 @@ Key: + + + Blur label ('intolerant') on quoted author account (hide) @@ -356,9 +452,11 @@ Key: + + ✋ @@ -366,6 +464,9 @@ Key: + + + Blur label ('intolerant') on post (warn) @@ -377,12 +478,17 @@ Key: + + + + + Blur label ('intolerant') on author profile (warn) @@ -390,6 +496,7 @@ Key: + ✋ @@ -397,9 +504,13 @@ Key: + + + + Blur label ('intolerant') on author account (warn) @@ -415,9 +526,13 @@ Key: + + + + Blur label ('intolerant') on quoted post (warn) @@ -425,9 +540,11 @@ Key: + + ✋ @@ -435,6 +552,9 @@ Key: + + + Blur label ('intolerant') on quoted author account (warn) @@ -442,9 +562,11 @@ Key: + + ✋ @@ -452,6 +574,9 @@ Key: + + + Blur label ('intolerant') on post (ignore) @@ -459,15 +584,21 @@ Key: + + + + + + Blur label ('intolerant') on author profile (ignore) @@ -475,15 +606,21 @@ Key: + + + + + + Blur label ('intolerant') on author account (ignore) @@ -491,15 +628,21 @@ Key: + + + + + + Blur label ('intolerant') on quoted post (ignore) @@ -507,15 +650,21 @@ Key: + + + + + + Blur label ('intolerant') on quoted author account (ignore) @@ -523,15 +672,20 @@ Key: + + + + + ScenarioFilterContentAvatarEmbed Blur-media label ('porn') on post (hide) @@ -540,9 +694,11 @@ Key: + + ✋ @@ -550,6 +706,9 @@ Key: + + + Blur-media label ('porn') on author profile (hide) @@ -557,6 +716,7 @@ Key: + ✋ @@ -564,9 +724,13 @@ Key: + + + + Blur-media label ('porn') on author account (hide) @@ -574,6 +738,7 @@ Key: + ✋ @@ -585,6 +750,9 @@ Key: + + + Blur-media label ('porn') on quoted post (hide) @@ -592,9 +760,11 @@ Key: + + ✋ @@ -602,6 +772,9 @@ Key: + + + Blur-media label ('porn') on quoted author account (hide) @@ -609,15 +782,21 @@ Key: + + + + + + Blur-media label ('porn') on post (warn) @@ -625,9 +804,11 @@ Key: + + ✋ @@ -635,6 +816,9 @@ Key: + + + Blur-media label ('porn') on author profile (warn) @@ -642,6 +826,7 @@ Key: + ✋ @@ -649,9 +834,13 @@ Key: + + + + Blur-media label ('porn') on author account (warn) @@ -659,6 +848,7 @@ Key: + ✋ @@ -670,6 +860,9 @@ Key: + + + Blur-media label ('porn') on quoted post (warn) @@ -677,9 +870,11 @@ Key: + + ✋ @@ -687,6 +882,9 @@ Key: + + + Blur-media label ('porn') on quoted author account (warn) @@ -694,15 +892,21 @@ Key: + + + + + + Blur-media label ('porn') on post (ignore) @@ -710,15 +914,21 @@ Key: + + + + + + Blur-media label ('porn') on author profile (ignore) @@ -726,15 +936,21 @@ Key: + + + + + + Blur-media label ('porn') on author account (ignore) @@ -742,15 +958,21 @@ Key: + + + + + + Blur-media label ('porn') on quoted post (ignore) @@ -758,15 +980,21 @@ Key: + + + + + + Blur-media label ('porn') on quoted author account (ignore) @@ -774,15 +1002,20 @@ Key: + + + + + ScenarioFilterContentAvatarEmbed Notice label ('scam') on post (hide) @@ -792,16 +1025,20 @@ Key: 🪧 - + + + + + Notice label ('scam') on author profile (hide) @@ -809,17 +1046,21 @@ Key: + 🪧 - + + + + Notice label ('scam') on author account (hide) @@ -828,18 +1069,20 @@ Key: 🪧 - 🪧 - + + + + Notice label ('scam') on quoted post (hide) @@ -847,17 +1090,21 @@ Key: + + 🪧 - + + + Notice label ('scam') on quoted author account (hide) @@ -865,17 +1112,21 @@ Key: + + 🪧 - + + + Notice label ('scam') on post (warn) @@ -884,16 +1135,20 @@ Key: 🪧 - + + + + + Notice label ('scam') on author profile (warn) @@ -901,17 +1156,21 @@ Key: + 🪧 - + + + + Notice label ('scam') on author account (warn) @@ -920,18 +1179,20 @@ Key: 🪧 - 🪧 - + + + + Notice label ('scam') on quoted post (warn) @@ -939,17 +1200,21 @@ Key: + + 🪧 - + + + Notice label ('scam') on quoted author account (warn) @@ -957,17 +1222,21 @@ Key: + + 🪧 - + + + Notice label ('scam') on post (ignore) @@ -975,15 +1244,21 @@ Key: + + + + + + Notice label ('scam') on author profile (ignore) @@ -991,15 +1266,21 @@ Key: + + + + + + Notice label ('scam') on author account (ignore) @@ -1007,15 +1288,21 @@ Key: + + + + + + Notice label ('scam') on quoted post (ignore) @@ -1023,15 +1310,21 @@ Key: + + + + + + Notice label ('scam') on quoted author account (ignore) @@ -1039,15 +1332,20 @@ Key: + + + + + ScenarioFilterContentAvatarEmbed Adult-only label on post when adult content is disabled @@ -1056,9 +1354,11 @@ Key: + + 🚫 @@ -1066,6 +1366,9 @@ Key: + + + Adult-only label on author profile when adult content is disabled @@ -1073,6 +1376,7 @@ Key: + 🚫 @@ -1080,9 +1384,13 @@ Key: + + + + Adult-only label on author account when adult content is disabled @@ -1090,6 +1398,7 @@ Key: + 🚫 @@ -1101,6 +1410,9 @@ Key: + + + Adult-only label on quoted post when adult content is disabled @@ -1108,9 +1420,11 @@ Key: + + 🚫 @@ -1118,6 +1432,9 @@ Key: + + + Adult-only label on quoted author account when adult content is disabled @@ -1125,15 +1442,20 @@ Key: + + + + + ScenarioFilterContentAvatarEmbed Self-post: Imperative label ('!hide') on post @@ -1146,12 +1468,17 @@ Key: + + + + + Self-post: Imperative label ('!hide') on author profile @@ -1159,15 +1486,21 @@ Key: + + + + + + Self-post: Imperative label ('!hide') on author account @@ -1175,15 +1508,21 @@ Key: + + + + + + Self-post: Imperative label ('!hide') on quoted post @@ -1191,9 +1530,11 @@ Key: + + ✋ @@ -1201,6 +1542,9 @@ Key: + + + Self-post: Imperative label ('!hide') on quoted author account @@ -1208,15 +1552,21 @@ Key: + + + + + + Self-post: Imperative label ('!warn') on post @@ -1228,12 +1578,17 @@ Key: + + + + + Self-post: Imperative label ('!warn') on author profile @@ -1241,15 +1596,21 @@ Key: + + + + + + Self-post: Imperative label ('!warn') on author account @@ -1257,15 +1618,21 @@ Key: + + + + + + Self-post: Imperative label ('!warn') on quoted post @@ -1273,9 +1640,11 @@ Key: + + ✋ @@ -1283,6 +1652,9 @@ Key: + + + Self-post: Imperative label ('!warn') on quoted author account @@ -1290,15 +1662,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on post (hide) @@ -1306,9 +1684,11 @@ Key: + + ✋ @@ -1316,6 +1696,9 @@ Key: + + + Self-post: Blur-media label ('porn') on author profile (hide) @@ -1323,15 +1706,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on author account (hide) @@ -1339,15 +1728,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on quoted post (hide) @@ -1355,9 +1750,11 @@ Key: + + ✋ @@ -1365,6 +1762,9 @@ Key: + + + Self-post: Blur-media label ('porn') on quoted author account (hide) @@ -1372,15 +1772,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on post (warn) @@ -1388,9 +1794,11 @@ Key: + + ✋ @@ -1398,6 +1806,9 @@ Key: + + + Self-post: Blur-media label ('porn') on author profile (warn) @@ -1405,15 +1816,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on author account (warn) @@ -1421,15 +1838,21 @@ Key: + + + + + + Self-post: Blur-media label ('porn') on quoted post (warn) @@ -1437,9 +1860,11 @@ Key: + + ✋ @@ -1447,6 +1872,9 @@ Key: + + + Self-post: Blur-media label ('porn') on quoted author account (warn) @@ -1454,15 +1882,20 @@ Key: + + + + + ScenarioFilterContentAvatarEmbed Post with blocked author @@ -1479,9 +1912,13 @@ Key: + + + + Post with blocked quoted author @@ -1489,9 +1926,11 @@ Key: + + 🚫 @@ -1499,6 +1938,9 @@ Key: + + + Post with author blocking user @@ -1514,9 +1956,13 @@ Key: + + + + Post with quoted author blocking user @@ -1524,9 +1970,11 @@ Key: + + 🚫 @@ -1534,6 +1982,9 @@ Key: + + + Post with muted author @@ -1545,12 +1996,17 @@ Key: + + + + + Post with muted quoted author @@ -1558,9 +2014,11 @@ Key: + + ✋ @@ -1568,6 +2026,9 @@ Key: + + + Post with muted-by-list author @@ -1579,12 +2040,17 @@ Key: + + + + + Post with muted-by-list quoted author @@ -1592,9 +2058,11 @@ Key: + + ✋ @@ -1602,6 +2070,8 @@ Key: + + ScenarioFilterContentAvatarEmbed Prioritization: post with blocking & blocked-by author @@ -1618,9 +2088,13 @@ Key: + + + + Prioritization: post with blocking & blocked-by quoted author @@ -1628,9 +2102,11 @@ Key: + + 🚫 @@ -1638,6 +2114,9 @@ Key: + + + Prioritization: '!hide' label on post by blocked user @@ -1653,9 +2132,13 @@ Key: + + + + Prioritization: '!hide' label on quoted post, post by blocked user @@ -1675,6 +2158,9 @@ Key: + + + Prioritization: '!hide' and 'intolerant' labels on post (hide) @@ -1686,12 +2172,17 @@ Key: + + + + + Prioritization: '!warn' and 'intolerant' labels on post (hide) @@ -1703,12 +2194,17 @@ Key: + + + + + Prioritization: '!hide' and 'porn' labels on post (hide) @@ -1720,12 +2216,17 @@ Key: + + + + + Prioritization: '!warn' and 'porn' labels on post (hide) @@ -1733,9 +2234,11 @@ Key: + + ✋ @@ -1743,4 +2246,4 @@ Key: - + \ No newline at end of file diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md index b8d7c94ce91..3d2f9af96b3 100644 --- a/packages/api/docs/moderation-behaviors/profiles.md +++ b/packages/api/docs/moderation-behaviors/profiles.md @@ -38,6 +38,7 @@ Key: + 🚫 @@ -45,6 +46,9 @@ Key: + + + Imperative label ('!hide') on profile @@ -52,6 +56,7 @@ Key: + 🚫 @@ -63,6 +68,9 @@ Key: + + + Imperative label ('!no-promote') on account @@ -70,15 +78,21 @@ Key: + + + + + + Imperative label ('!no-promote') on profile @@ -86,15 +100,21 @@ Key: + + + + + + Imperative label ('!warn') on account @@ -106,6 +126,7 @@ Key: + ✋ @@ -113,6 +134,9 @@ Key: + + + Imperative label ('!warn') on profile @@ -120,6 +144,7 @@ Key: + ✋ @@ -131,6 +156,8 @@ Key: + + ScenarioFilterAccountProfileAvatar Blur label ('intolerant') on account (hide) @@ -143,6 +170,7 @@ Key: + ✋ @@ -150,6 +178,9 @@ Key: + + + Blur label ('intolerant') on profile (hide) @@ -157,6 +188,7 @@ Key: + ✋ @@ -168,6 +200,9 @@ Key: + + + Blur label ('intolerant') on account (warn) @@ -179,6 +214,7 @@ Key: + ✋ @@ -186,6 +222,9 @@ Key: + + + Blur label ('intolerant') on profile (warn) @@ -193,6 +232,7 @@ Key: + ✋ @@ -204,6 +244,9 @@ Key: + + + Blur label ('intolerant') on account (ignore) @@ -211,15 +254,21 @@ Key: + + + + + + Blur label ('intolerant') on profile (ignore) @@ -227,15 +276,20 @@ Key: + + + + + ScenarioFilterAccountProfileAvatar Blur-media label ('porn') on account (hide) @@ -248,6 +302,7 @@ Key: + ✋ @@ -255,6 +310,9 @@ Key: + + + Blur-media label ('porn') on profile (hide) @@ -262,9 +320,11 @@ Key: + + ✋ @@ -272,6 +332,9 @@ Key: + + + Blur-media label ('porn') on account (warn) @@ -283,6 +346,7 @@ Key: + ✋ @@ -290,6 +354,9 @@ Key: + + + Blur-media label ('porn') on profile (warn) @@ -297,9 +364,11 @@ Key: + + ✋ @@ -307,6 +376,9 @@ Key: + + + Blur-media label ('porn') on account (ignore) @@ -314,15 +386,21 @@ Key: + + + + + + Blur-media label ('porn') on profile (ignore) @@ -330,15 +408,20 @@ Key: + + + + + ScenarioFilterAccountProfileAvatar Notice label ('scam') on account (hide) @@ -348,18 +431,20 @@ Key: 🪧 - + 🪧 - + + + Notice label ('scam') on profile (hide) @@ -367,19 +452,21 @@ Key: + 🪧 - 🪧 - + + + Notice label ('scam') on account (warn) @@ -388,18 +475,20 @@ Key: 🪧 - + 🪧 - + + + Notice label ('scam') on profile (warn) @@ -407,19 +496,21 @@ Key: + 🪧 - 🪧 - + + + Notice label ('scam') on account (ignore) @@ -427,15 +518,21 @@ Key: + + + + + + Notice label ('scam') on profile (ignore) @@ -443,15 +540,20 @@ Key: + + + + + ScenarioFilterAccountProfileAvatar Adult-only label on account when adult content is disabled @@ -464,6 +566,7 @@ Key: + 🚫 @@ -471,6 +574,9 @@ Key: + + + Adult-only label on profile when adult content is disabled @@ -478,9 +584,11 @@ Key: + + 🚫 @@ -488,6 +596,8 @@ Key: + + ScenarioFilterAccountProfileAvatar Self-profile: !hide on account @@ -497,18 +607,20 @@ Key: 🪧 - + 🪧 - + + + Self-profile: !hide on profile @@ -516,19 +628,20 @@ Key: + 🪧 - 🪧 - + + ScenarioFilterAccountProfileAvatar Mute/block: Blocking user @@ -537,9 +650,11 @@ Key: + + 🚫 @@ -547,6 +662,9 @@ Key: + + + Mute/block: Blocked by user @@ -554,9 +672,11 @@ Key: + + 🚫 @@ -564,6 +684,9 @@ Key: + + + Mute/block: Muted user @@ -571,15 +694,21 @@ Key: + + + + + + Mute/block: Muted-by-list user @@ -587,15 +716,20 @@ Key: + + + + + ScenarioFilterAccountProfileAvatar Prioritization: blocking & blocked-by user @@ -604,9 +738,11 @@ Key: + + 🚫 @@ -614,6 +750,9 @@ Key: + + + Prioritization: '!hide' label on account of blocked user @@ -625,6 +764,7 @@ Key: + 🚫 @@ -632,6 +772,9 @@ Key: + + + Prioritization: '!hide' and 'intolerant' labels on account (hide) @@ -643,6 +786,7 @@ Key: + 🚫 @@ -650,6 +794,9 @@ Key: + + + Prioritization: '!warn' and 'intolerant' labels on account (hide) @@ -661,6 +808,7 @@ Key: + ✋ @@ -668,6 +816,9 @@ Key: + + + Prioritization: '!warn' and 'porn' labels on account (hide) @@ -679,6 +830,7 @@ Key: + ✋ @@ -686,6 +838,9 @@ Key: + + + Prioritization: intolerant label on account (hide) and scam label on profile (warn) @@ -698,7 +853,6 @@ Key: 🪧 - ✋ @@ -706,6 +860,9 @@ Key: + + + Prioritization: !hide on account, !warn on profile @@ -725,6 +882,9 @@ Key: + + + Prioritization: !warn on account, !hide on profile @@ -744,4 +904,4 @@ Key: - + \ No newline at end of file diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index f9ebf0ade63..768ab3e087e 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -41,6 +41,7 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' @@ -55,9 +56,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete' +import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation' +import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate' import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset' import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword' import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword' +import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail' import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' @@ -163,6 +167,7 @@ export * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +export * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' export * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' export * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' @@ -177,9 +182,12 @@ export * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi export * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' export * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' export * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete' +export * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation' +export * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate' export * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset' export * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword' export * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword' +export * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail' export * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' export * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' export * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' @@ -699,6 +707,17 @@ export class ServerNS { this._service = service } + confirmEmail( + data?: ComAtprotoServerConfirmEmail.InputSchema, + opts?: ComAtprotoServerConfirmEmail.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.confirmEmail', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerConfirmEmail.toKnownErr(e) + }) + } + createAccount( data?: ComAtprotoServerCreateAccount.InputSchema, opts?: ComAtprotoServerCreateAccount.CallOptions, @@ -842,6 +861,28 @@ export class ServerNS { }) } + requestEmailConfirmation( + data?: ComAtprotoServerRequestEmailConfirmation.InputSchema, + opts?: ComAtprotoServerRequestEmailConfirmation.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.requestEmailConfirmation', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerRequestEmailConfirmation.toKnownErr(e) + }) + } + + requestEmailUpdate( + data?: ComAtprotoServerRequestEmailUpdate.InputSchema, + opts?: ComAtprotoServerRequestEmailUpdate.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.requestEmailUpdate', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerRequestEmailUpdate.toKnownErr(e) + }) + } + requestPasswordReset( data?: ComAtprotoServerRequestPasswordReset.InputSchema, opts?: ComAtprotoServerRequestPasswordReset.CallOptions, @@ -874,6 +915,17 @@ export class ServerNS { throw ComAtprotoServerRevokeAppPassword.toKnownErr(e) }) } + + updateEmail( + data?: ComAtprotoServerUpdateEmail.InputSchema, + opts?: ComAtprotoServerUpdateEmail.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.server.updateEmail', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoServerUpdateEmail.toKnownErr(e) + }) + } } export class SyncNS { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index e15d7aba1ce..17ad93b38c7 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2238,6 +2238,40 @@ export const schemaDict = { }, }, }, + ComAtprotoServerConfirmEmail: { + lexicon: 1, + id: 'com.atproto.server.confirmEmail', + defs: { + main: { + type: 'procedure', + description: + 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email', 'token'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoServerCreateAccount: { lexicon: 1, id: 'com.atproto.server.createAccount', @@ -2850,6 +2884,27 @@ export const schemaDict = { }, }, }, + ComAtprotoServerRequestEmailConfirmation: { + lexicon: 1, + id: 'com.atproto.server.requestEmailConfirmation', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to confirm ownership of email', + }, + }, + }, + ComAtprotoServerRequestEmailUpdate: { + lexicon: 1, + id: 'com.atproto.server.requestEmailUpdate', + defs: { + main: { + type: 'procedure', + description: 'Request a token in order to update email.', + }, + }, + }, ComAtprotoServerRequestPasswordReset: { lexicon: 1, id: 'com.atproto.server.requestPasswordReset', @@ -2927,6 +2982,41 @@ export const schemaDict = { }, }, }, + ComAtprotoServerUpdateEmail: { + lexicon: 1, + id: 'com.atproto.server.updateEmail', + defs: { + main: { + type: 'procedure', + description: "Update an account's email.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + description: + " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoSyncGetBlob: { lexicon: 1, id: 'com.atproto.sync.getBlob', @@ -6715,6 +6805,7 @@ export const ids = { ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', @@ -6731,10 +6822,14 @@ export const ids = { ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', ComAtprotoServerRequestAccountDelete: 'com.atproto.server.requestAccountDelete', + ComAtprotoServerRequestEmailConfirmation: + 'com.atproto.server.requestEmailConfirmation', + ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate', ComAtprotoServerRequestPasswordReset: 'com.atproto.server.requestPasswordReset', ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword', ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword', + ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail', ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', diff --git a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts new file mode 100644 index 00000000000..64a9d6811bb --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + email: string + token: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export class ExpiredTokenError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export class InvalidTokenError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'ExpiredToken') return new ExpiredTokenError(e) + if (e.error === 'InvalidToken') return new InvalidTokenError(e) + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts b/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts new file mode 100644 index 00000000000..ef2ed1ac47c --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts @@ -0,0 +1,28 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface CallOptions { + headers?: Headers + qp?: QueryParams +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts new file mode 100644 index 00000000000..ef2ed1ac47c --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts @@ -0,0 +1,28 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface CallOptions { + headers?: Headers + qp?: QueryParams +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts new file mode 100644 index 00000000000..6b7c476d0a9 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + email: string + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + token?: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export class ExpiredTokenError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export class InvalidTokenError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'ExpiredToken') return new ExpiredTokenError(e) + if (e.error === 'InvalidToken') return new InvalidTokenError(e) + } + return e +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index a99d4d6e51b..be1b593b5ef 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -39,6 +39,7 @@ import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' @@ -52,9 +53,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete' +import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation' +import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate' import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset' import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword' import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword' +import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail' import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' @@ -554,6 +558,17 @@ export class ServerNS { this._server = server } + confirmEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerConfirmEmail.Handler>, + ComAtprotoServerConfirmEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + createAccount( cfg: ConfigOf< AV, @@ -697,6 +712,28 @@ export class ServerNS { return this._server.xrpc.method(nsid, cfg) } + requestEmailConfirmation( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailConfirmation.Handler>, + ComAtprotoServerRequestEmailConfirmation.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestEmailUpdate( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailUpdate.Handler>, + ComAtprotoServerRequestEmailUpdate.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + requestPasswordReset( cfg: ConfigOf< AV, @@ -729,6 +766,17 @@ export class ServerNS { const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + updateEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerUpdateEmail.Handler>, + ComAtprotoServerUpdateEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.updateEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } export class SyncNS { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index e15d7aba1ce..17ad93b38c7 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2238,6 +2238,40 @@ export const schemaDict = { }, }, }, + ComAtprotoServerConfirmEmail: { + lexicon: 1, + id: 'com.atproto.server.confirmEmail', + defs: { + main: { + type: 'procedure', + description: + 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email', 'token'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoServerCreateAccount: { lexicon: 1, id: 'com.atproto.server.createAccount', @@ -2850,6 +2884,27 @@ export const schemaDict = { }, }, }, + ComAtprotoServerRequestEmailConfirmation: { + lexicon: 1, + id: 'com.atproto.server.requestEmailConfirmation', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to confirm ownership of email', + }, + }, + }, + ComAtprotoServerRequestEmailUpdate: { + lexicon: 1, + id: 'com.atproto.server.requestEmailUpdate', + defs: { + main: { + type: 'procedure', + description: 'Request a token in order to update email.', + }, + }, + }, ComAtprotoServerRequestPasswordReset: { lexicon: 1, id: 'com.atproto.server.requestPasswordReset', @@ -2927,6 +2982,41 @@ export const schemaDict = { }, }, }, + ComAtprotoServerUpdateEmail: { + lexicon: 1, + id: 'com.atproto.server.updateEmail', + defs: { + main: { + type: 'procedure', + description: "Update an account's email.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + description: + " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoSyncGetBlob: { lexicon: 1, id: 'com.atproto.sync.getBlob', @@ -6715,6 +6805,7 @@ export const ids = { ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', @@ -6731,10 +6822,14 @@ export const ids = { ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', ComAtprotoServerRequestAccountDelete: 'com.atproto.server.requestAccountDelete', + ComAtprotoServerRequestEmailConfirmation: + 'com.atproto.server.requestEmailConfirmation', + ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate', ComAtprotoServerRequestPasswordReset: 'com.atproto.server.requestPasswordReset', ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword', ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword', + ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail', ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts new file mode 100644 index 00000000000..6d2d89db7d0 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + email: string + token: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts new file mode 100644 index 00000000000..c2c99ea01c8 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + email: string + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + token?: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index a99d4d6e51b..be1b593b5ef 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -39,6 +39,7 @@ import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' @@ -52,9 +53,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete' +import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation' +import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate' import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset' import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword' import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword' +import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail' import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' @@ -554,6 +558,17 @@ export class ServerNS { this._server = server } + confirmEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerConfirmEmail.Handler>, + ComAtprotoServerConfirmEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + createAccount( cfg: ConfigOf< AV, @@ -697,6 +712,28 @@ export class ServerNS { return this._server.xrpc.method(nsid, cfg) } + requestEmailConfirmation( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailConfirmation.Handler>, + ComAtprotoServerRequestEmailConfirmation.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestEmailUpdate( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailUpdate.Handler>, + ComAtprotoServerRequestEmailUpdate.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + requestPasswordReset( cfg: ConfigOf< AV, @@ -729,6 +766,17 @@ export class ServerNS { const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + updateEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerUpdateEmail.Handler>, + ComAtprotoServerUpdateEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.updateEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } export class SyncNS { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index e15d7aba1ce..17ad93b38c7 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2238,6 +2238,40 @@ export const schemaDict = { }, }, }, + ComAtprotoServerConfirmEmail: { + lexicon: 1, + id: 'com.atproto.server.confirmEmail', + defs: { + main: { + type: 'procedure', + description: + 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email', 'token'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoServerCreateAccount: { lexicon: 1, id: 'com.atproto.server.createAccount', @@ -2850,6 +2884,27 @@ export const schemaDict = { }, }, }, + ComAtprotoServerRequestEmailConfirmation: { + lexicon: 1, + id: 'com.atproto.server.requestEmailConfirmation', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to confirm ownership of email', + }, + }, + }, + ComAtprotoServerRequestEmailUpdate: { + lexicon: 1, + id: 'com.atproto.server.requestEmailUpdate', + defs: { + main: { + type: 'procedure', + description: 'Request a token in order to update email.', + }, + }, + }, ComAtprotoServerRequestPasswordReset: { lexicon: 1, id: 'com.atproto.server.requestPasswordReset', @@ -2927,6 +2982,41 @@ export const schemaDict = { }, }, }, + ComAtprotoServerUpdateEmail: { + lexicon: 1, + id: 'com.atproto.server.updateEmail', + defs: { + main: { + type: 'procedure', + description: "Update an account's email.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + description: + " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, ComAtprotoSyncGetBlob: { lexicon: 1, id: 'com.atproto.sync.getBlob', @@ -6715,6 +6805,7 @@ export const ids = { ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', @@ -6731,10 +6822,14 @@ export const ids = { ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', ComAtprotoServerRequestAccountDelete: 'com.atproto.server.requestAccountDelete', + ComAtprotoServerRequestEmailConfirmation: + 'com.atproto.server.requestEmailConfirmation', + ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate', ComAtprotoServerRequestPasswordReset: 'com.atproto.server.requestPasswordReset', ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword', ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword', + ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail', ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', diff --git a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts new file mode 100644 index 00000000000..6d2d89db7d0 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + email: string + token: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts new file mode 100644 index 00000000000..c2c99ea01c8 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + email: string + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + token?: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput From ea1e6cceaadd20ed0ef57e00115775d86ddc340c Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 16:46:20 -0500 Subject: [PATCH 03/18] email templates --- packages/pds/src/mailer/index.ts | 16 + .../src/mailer/templates/confirm-email.hbs | 382 ++++++++++++++++++ .../pds/src/mailer/templates/update-email.hbs | 381 +++++++++++++++++ 3 files changed, 779 insertions(+) create mode 100644 packages/pds/src/mailer/templates/confirm-email.hbs create mode 100644 packages/pds/src/mailer/templates/update-email.hbs diff --git a/packages/pds/src/mailer/index.ts b/packages/pds/src/mailer/index.ts index 99059f6f02e..6c77fc8964c 100644 --- a/packages/pds/src/mailer/index.ts +++ b/packages/pds/src/mailer/index.ts @@ -24,6 +24,8 @@ export class ServerMailer { this.templates = { resetPassword: this.compile('reset-password'), deleteAccount: this.compile('delete-account'), + confirmEmail: this.compile('confirm-email'), + updateEmail: this.compile('update-email'), } } @@ -51,6 +53,20 @@ export class ServerMailer { }) } + async sendConfirmEmail(params: { token: string }, mailOpts: Mail.Options) { + return this.sendTemplate('confirmEmail', params, { + subject: 'Email Confirmation', + ...mailOpts, + }) + } + + async sendUpdateEmail(params: { token: string }, mailOpts: Mail.Options) { + return this.sendTemplate('updateEmail', params, { + subject: 'Email Update Requested', + ...mailOpts, + }) + } + private async sendTemplate(templateName, params, mailOpts: Mail.Options) { const html = this.templates[templateName]({ ...params, diff --git a/packages/pds/src/mailer/templates/confirm-email.hbs b/packages/pds/src/mailer/templates/confirm-email.hbs new file mode 100644 index 00000000000..cf0f123b0f6 --- /dev/null +++ b/packages/pds/src/mailer/templates/confirm-email.hbs @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/pds/src/mailer/templates/update-email.hbs b/packages/pds/src/mailer/templates/update-email.hbs new file mode 100644 index 00000000000..a7c22fd5ca6 --- /dev/null +++ b/packages/pds/src/mailer/templates/update-email.hbs @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From c4cf9fab664bcb226d6e5e9f9ef7a3e4a66eb24a Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 16:50:21 -0500 Subject: [PATCH 04/18] request routes --- .../atproto/server/requestEmailUpdate.json | 12 +++++- packages/api/src/client/lexicons.ts | 12 ++++++ .../com/atproto/server/requestEmailUpdate.ts | 6 +++ packages/bsky/src/lexicon/lexicons.ts | 12 ++++++ .../com/atproto/server/requestEmailUpdate.ts | 14 ++++++- .../atproto/server/requestAccountDelete.ts | 4 +- .../server/requestEmailConfirmation.ts | 27 +++++++++++++ .../com/atproto/server/requestEmailUpdate.ts | 38 +++++++++++++++++++ packages/pds/src/db/database-schema.ts | 2 + packages/pds/src/db/tables/email-token.ts | 14 +++++++ packages/pds/src/db/tables/user-account.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 12 ++++++ .../com/atproto/server/requestEmailUpdate.ts | 14 ++++++- 13 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts create mode 100644 packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts create mode 100644 packages/pds/src/db/tables/email-token.ts diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json index 37c435d72e5..4cc1a86f612 100644 --- a/lexicons/com/atproto/server/requestEmailUpdate.json +++ b/lexicons/com/atproto/server/requestEmailUpdate.json @@ -4,7 +4,17 @@ "defs": { "main": { "type": "procedure", - "description": "Request a token in order to update email." + "description": "Request a token in order to update email.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["tokenRequired"], + "properties": { + "tokenRequired": { "type": "boolean" } + } + } + } } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 17ad93b38c7..ff72487df77 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2902,6 +2902,18 @@ export const schemaDict = { main: { type: 'procedure', description: 'Request a token in order to update email.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['tokenRequired'], + properties: { + tokenRequired: { + type: 'boolean', + }, + }, + }, + }, }, }, }, diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts index ef2ed1ac47c..30d84002cf2 100644 --- a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts +++ b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts @@ -11,6 +11,11 @@ export interface QueryParams {} export type InputSchema = undefined +export interface OutputSchema { + tokenRequired: boolean + [k: string]: unknown +} + export interface CallOptions { headers?: Headers qp?: QueryParams @@ -19,6 +24,7 @@ export interface CallOptions { export interface Response { success: boolean headers: Headers + data: OutputSchema } export function toKnownErr(e: any) { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 17ad93b38c7..ff72487df77 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2902,6 +2902,18 @@ export const schemaDict = { main: { type: 'procedure', description: 'Request a token in order to update email.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['tokenRequired'], + properties: { + tokenRequired: { + type: 'boolean', + }, + }, + }, + }, }, }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts index e4244870425..6876d44ca46 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -11,14 +11,26 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams {} export type InputSchema = undefined + +export interface OutputSchema { + tokenRequired: boolean + [k: string]: unknown +} + export type HandlerInput = undefined +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + export interface HandlerError { status: number message?: string } -export type HandlerOutput = HandlerError | void +export type HandlerOutput = HandlerError | HandlerSuccess export type HandlerReqCtx = { auth: HA params: QueryParams diff --git a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts index 61870c8c3ca..a448d97c02e 100644 --- a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts +++ b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts @@ -8,12 +8,12 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifierCheckTakedown, handler: async ({ auth }) => { const did = auth.credentials.did - const token = getRandomToken().toUpperCase() - const requestedAt = new Date().toISOString() const user = await ctx.services.account(ctx.db).getAccount(did) if (!user) { throw new InvalidRequestError('user not found') } + const token = getRandomToken().toUpperCase() + const requestedAt = new Date().toISOString() await ctx.db.db .insertInto('delete_account_token') .values({ did, token, requestedAt }) diff --git a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts new file mode 100644 index 00000000000..3d2299bd28b --- /dev/null +++ b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts @@ -0,0 +1,27 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { getRandomToken } from './util' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.server.requestEmailConfirmation({ + auth: ctx.accessVerifierCheckTakedown, + handler: async ({ auth }) => { + const did = auth.credentials.did + const user = await ctx.services.account(ctx.db).getAccount(did) + if (!user) { + throw new InvalidRequestError('user not found') + } + const token = getRandomToken().toUpperCase() + const requestedAt = new Date().toISOString() + await ctx.db.db + .insertInto('email_token') + .values({ purpose: 'confirm_email', did, token, requestedAt }) + .onConflict((oc) => + oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }), + ) + .execute() + await ctx.mailer.sendConfirmEmail({ token }, { to: user.email }) + }, + }) +} diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts new file mode 100644 index 00000000000..e2fa5f36b7a --- /dev/null +++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts @@ -0,0 +1,38 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { getRandomToken } from './util' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.server.requestEmailUpdate({ + auth: ctx.accessVerifierCheckTakedown, + handler: async ({ auth }) => { + const did = auth.credentials.did + const user = await ctx.services.account(ctx.db).getAccount(did) + if (!user) { + throw new InvalidRequestError('user not found') + } + + const tokenRequired = user.emailConfirmedAt !== null + if (tokenRequired) { + const token = getRandomToken().toUpperCase() + const requestedAt = new Date().toISOString() + await ctx.db.db + .insertInto('email_token') + .values({ purpose: 'update_email', did, token, requestedAt }) + .onConflict((oc) => + oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }), + ) + .execute() + await ctx.mailer.sendUpdateEmail({ token }, { to: user.email }) + } + + return { + encoding: 'application/json', + body: { + tokenRequired, + }, + } + }, + }) +} diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index f77df6b21ad..bebb13eb84d 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -15,6 +15,7 @@ import * as notification from './tables/user-notification' import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' import * as deleteAccountToken from './tables/delete-account-token' +import * as emailToken from './tables/email-token' import * as moderation from './tables/moderation' import * as mute from './tables/mute' import * as listMute from './tables/list-mute' @@ -43,6 +44,7 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType & blob.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & + emailToken.PartialDB & moderation.PartialDB & mute.PartialDB & listMute.PartialDB & diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts new file mode 100644 index 00000000000..01679d83e97 --- /dev/null +++ b/packages/pds/src/db/tables/email-token.ts @@ -0,0 +1,14 @@ +export interface EmailToken { + purpose: + | 'confirm_email' + | 'update_email' + | 'reset_password' + | 'delete_account' + did: string + token: string + requestedAt: string +} + +export const tableName = 'email_token' + +export type PartialDB = { [tableName]: EmailToken } diff --git a/packages/pds/src/db/tables/user-account.ts b/packages/pds/src/db/tables/user-account.ts index 665521efc08..ef9fdbecb3c 100644 --- a/packages/pds/src/db/tables/user-account.ts +++ b/packages/pds/src/db/tables/user-account.ts @@ -5,6 +5,7 @@ export interface UserAccount { email: string passwordScrypt: string createdAt: string + emailConfirmedAt: string | null passwordResetToken: string | null passwordResetGrantedAt: string | null invitesDisabled: Generated<0 | 1> diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 17ad93b38c7..ff72487df77 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2902,6 +2902,18 @@ export const schemaDict = { main: { type: 'procedure', description: 'Request a token in order to update email.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['tokenRequired'], + properties: { + tokenRequired: { + type: 'boolean', + }, + }, + }, + }, }, }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts index e4244870425..6876d44ca46 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -11,14 +11,26 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams {} export type InputSchema = undefined + +export interface OutputSchema { + tokenRequired: boolean + [k: string]: unknown +} + export type HandlerInput = undefined +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + export interface HandlerError { status: number message?: string } -export type HandlerOutput = HandlerError | void +export type HandlerOutput = HandlerError | HandlerSuccess export type HandlerReqCtx = { auth: HA params: QueryParams From 57aec2267d6359aeafb0b672873dc590f510ff6e Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 17:19:09 -0500 Subject: [PATCH 05/18] impl --- packages/common-web/src/times.ts | 4 ++ .../api/com/atproto/server/confirmEmail.ts | 25 ++++++++++ .../pds/src/api/com/atproto/server/index.ts | 10 ++++ .../server/requestEmailConfirmation.ts | 13 ++--- .../com/atproto/server/requestEmailUpdate.ts | 13 ++--- .../src/api/com/atproto/server/updateEmail.ts | 42 +++++++++++++++++ packages/pds/src/db/tables/email-token.ts | 16 ++++--- packages/pds/src/services/account/index.ts | 47 ++++++++++++++++++- 8 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 packages/pds/src/api/com/atproto/server/confirmEmail.ts create mode 100644 packages/pds/src/api/com/atproto/server/updateEmail.ts diff --git a/packages/common-web/src/times.ts b/packages/common-web/src/times.ts index 09bb26efc97..90366277fdf 100644 --- a/packages/common-web/src/times.ts +++ b/packages/common-web/src/times.ts @@ -2,3 +2,7 @@ export const SECOND = 1000 export const MINUTE = SECOND * 60 export const HOUR = MINUTE * 60 export const DAY = HOUR * 24 + +export const lessThanAgoMs = (time: Date, range: number) => { + return Date.now() < time.getTime() + range +} diff --git a/packages/pds/src/api/com/atproto/server/confirmEmail.ts b/packages/pds/src/api/com/atproto/server/confirmEmail.ts new file mode 100644 index 00000000000..3e42931ae0f --- /dev/null +++ b/packages/pds/src/api/com/atproto/server/confirmEmail.ts @@ -0,0 +1,25 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.server.confirmEmail({ + auth: ctx.accessVerifierCheckTakedown, + handler: async ({ auth, input }) => { + const did = auth.credentials.did + const { token } = input.body + + await ctx.services + .account(ctx.db) + .assertValidToken(did, 'confirm_email', token) + + await ctx.db.transaction(async (dbTxn) => { + await ctx.services.account(dbTxn).deleteEmailToken(did, 'confirm_email') + await dbTxn.db + .updateTable('user_account') + .set({ emailConfirmedAt: new Date().toISOString() }) + .where('did', '=', did) + .execute() + }) + }, + }) +} diff --git a/packages/pds/src/api/com/atproto/server/index.ts b/packages/pds/src/api/com/atproto/server/index.ts index 9a49216f71c..210d0f45461 100644 --- a/packages/pds/src/api/com/atproto/server/index.ts +++ b/packages/pds/src/api/com/atproto/server/index.ts @@ -14,6 +14,12 @@ import deleteAccount from './deleteAccount' import requestPasswordReset from './requestPasswordReset' import resetPassword from './resetPassword' +import requestEmailConfirmation from './requestEmailConfirmation' +import confirmEmail from './confirmEmail' + +import requestEmailUpdate from './requestEmailUpdate' +import updateEmail from './updateEmail' + import createSession from './createSession' import deleteSession from './deleteSession' import getSession from './getSession' @@ -33,6 +39,10 @@ export default function (server: Server, ctx: AppContext) { deleteAccount(server, ctx) requestPasswordReset(server, ctx) resetPassword(server, ctx) + requestEmailConfirmation(server, ctx) + confirmEmail(server, ctx) + requestEmailUpdate(server, ctx) + updateEmail(server, ctx) createSession(server, ctx) deleteSession(server, ctx) getSession(server, ctx) diff --git a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts index 3d2299bd28b..aa7b632569e 100644 --- a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts +++ b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts @@ -1,7 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { getRandomToken } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.requestEmailConfirmation({ @@ -12,15 +11,9 @@ export default function (server: Server, ctx: AppContext) { if (!user) { throw new InvalidRequestError('user not found') } - const token = getRandomToken().toUpperCase() - const requestedAt = new Date().toISOString() - await ctx.db.db - .insertInto('email_token') - .values({ purpose: 'confirm_email', did, token, requestedAt }) - .onConflict((oc) => - oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }), - ) - .execute() + const token = await ctx.services + .account(ctx.db) + .createEmailToken(did, 'confirm_email') await ctx.mailer.sendConfirmEmail({ token }, { to: user.email }) }, }) diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts index e2fa5f36b7a..e38b69a6d7e 100644 --- a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts +++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts @@ -1,7 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { getRandomToken } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.requestEmailUpdate({ @@ -15,15 +14,9 @@ export default function (server: Server, ctx: AppContext) { const tokenRequired = user.emailConfirmedAt !== null if (tokenRequired) { - const token = getRandomToken().toUpperCase() - const requestedAt = new Date().toISOString() - await ctx.db.db - .insertInto('email_token') - .values({ purpose: 'update_email', did, token, requestedAt }) - .onConflict((oc) => - oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }), - ) - .execute() + const token = await ctx.services + .account(ctx.db) + .createEmailToken(did, 'update_email') await ctx.mailer.sendUpdateEmail({ token }, { to: user.email }) } diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts new file mode 100644 index 00000000000..d89c45cbbac --- /dev/null +++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts @@ -0,0 +1,42 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.server.updateEmail({ + auth: ctx.accessVerifierCheckTakedown, + handler: async ({ auth, input }) => { + const did = auth.credentials.did + const { token, email } = input.body + const user = await ctx.services.account(ctx.db).getAccount(did) + if (!user) { + throw new InvalidRequestError('user not found') + } + // require valid token + if (user.emailConfirmedAt !== null) { + if (!token) { + throw new InvalidRequestError( + 'confirmation token required', + 'TokenRequired', + ) + } + await ctx.services + .account(ctx.db) + .assertValidToken(did, 'confirm_email', token) + } + + await ctx.db.transaction(async (dbTxn) => { + if (token) { + await ctx.services + .account(dbTxn) + .deleteEmailToken(did, 'update_email') + } + await dbTxn.db + .updateTable('user_account') + .set({ email, emailConfirmedAt: null }) + .where('did', '=', did) + .execute() + }) + }, + }) +} diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts index 01679d83e97..d962bbf1cc5 100644 --- a/packages/pds/src/db/tables/email-token.ts +++ b/packages/pds/src/db/tables/email-token.ts @@ -1,12 +1,16 @@ +import { Generated } from 'kysely' + +export type EmailTokenPurpose = + | 'confirm_email' + | 'update_email' + | 'reset_password' + | 'delete_account' + export interface EmailToken { - purpose: - | 'confirm_email' - | 'update_email' - | 'reset_password' - | 'delete_account' + purpose: EmailTokenPurpose did: string token: string - requestedAt: string + requestedAt: Generated } export const tableName = 'email_token' diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index dd28af01df5..3d955f8d1c9 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -12,7 +12,9 @@ import * as sequencer from '../../sequencer' import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword' import { randomStr } from '@atproto/crypto' import { InvalidRequestError } from '@atproto/xrpc-server' -import { NotEmptyArray } from '@atproto/common' +import { MINUTE, NotEmptyArray, lessThanAgoMs } from '@atproto/common' +import { EmailTokenPurpose } from '../../db/tables/email-token' +import { getRandomToken } from '../../api/com/atproto/server/util' export class AccountService { constructor(public db: Database) {} @@ -555,6 +557,49 @@ export class AccountService { }, {} as Record) } + async createEmailToken( + did: string, + purpose: EmailTokenPurpose, + ): Promise { + const token = getRandomToken().toUpperCase() + await this.db.db + .insertInto('email_token') + .values({ purpose, did, token }) + .onConflict((oc) => oc.columns(['purpose', 'did']).doUpdateSet({ token })) + .execute() + return token + } + + async deleteEmailToken(did: string, purpose: EmailTokenPurpose) { + await this.db.db + .deleteFrom('email_token') + .where('did', '=', did) + .where('purpose', '=', purpose) + .executeTakeFirst() + } + + async assertValidToken( + did: string, + purpose: EmailTokenPurpose, + token: string, + expirationLen = 15 * MINUTE, + ) { + const res = await this.db.db + .selectFrom('email_token') + .selectAll() + .where('purpose', '=', purpose) + .where('did', '=', did) + .where('token', '=', token) + .executeTakeFirst() + if (!res) { + throw new InvalidRequestError('Token is invalid', 'InvalidToken') + } + const expired = !lessThanAgoMs(res.requestedAt, expirationLen) + if (expired) { + throw new InvalidRequestError('Token is expired', 'ExpiredToken') + } + } + async getLastSeenNotifs(did: string): Promise { const res = await this.db.db .selectFrom('user_state') From 5dfd97f81dd6247ef18aa423de930a2e65e7f606 Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 17:50:26 -0500 Subject: [PATCH 06/18] migration --- .../20230908T224408678Z-email-tokens.ts | 18 ++++++++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/email-token.ts | 4 +--- packages/pds/src/services/account/index.ts | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts new file mode 100644 index 00000000000..f32eef3f54e --- /dev/null +++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts @@ -0,0 +1,18 @@ +import { Kysely } from 'kysely' +import { Dialect } from '..' + +export async function up(db: Kysely, dialect: Dialect): Promise { + const timestamp = dialect === 'sqlite' ? 'datetime' : 'timestamptz' + await db.schema + .createTable('email_token') + .addColumn('purpose', 'varchar', (col) => col.notNull()) + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('token', 'varchar', (col) => col.notNull()) + .addColumn('requestedAt', timestamp, (col) => col.notNull()) + .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('email_token').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index e7e521e986a..f4401936e34 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -65,3 +65,4 @@ export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy' export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx' export * as _20230828T153013575Z from './20230828T153013575Z-repo-history-rewrite' +export * as _20230908T224408678Z from './20230908T224408678Z-email-tokens' diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts index d962bbf1cc5..b8f42bde198 100644 --- a/packages/pds/src/db/tables/email-token.ts +++ b/packages/pds/src/db/tables/email-token.ts @@ -1,5 +1,3 @@ -import { Generated } from 'kysely' - export type EmailTokenPurpose = | 'confirm_email' | 'update_email' @@ -10,7 +8,7 @@ export interface EmailToken { purpose: EmailTokenPurpose did: string token: string - requestedAt: Generated + requestedAt: Date } export const tableName = 'email_token' diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 3d955f8d1c9..59115c9b6e5 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -564,7 +564,7 @@ export class AccountService { const token = getRandomToken().toUpperCase() await this.db.db .insertInto('email_token') - .values({ purpose, did, token }) + .values({ purpose, did, token, requestedAt: new Date() }) .onConflict((oc) => oc.columns(['purpose', 'did']).doUpdateSet({ token })) .execute() return token From a80966f56083f7ce2d296d4c44b21ffb6087e165 Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 17:53:09 -0500 Subject: [PATCH 07/18] tidy --- .../pds/src/api/com/atproto/server/updateEmail.ts | 14 ++++++-------- packages/pds/src/services/account/index.ts | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts index d89c45cbbac..095b89ce2b0 100644 --- a/packages/pds/src/api/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts @@ -26,16 +26,14 @@ export default function (server: Server, ctx: AppContext) { } await ctx.db.transaction(async (dbTxn) => { + const accntSrvce = ctx.services.account(dbTxn) + if (token) { - await ctx.services - .account(dbTxn) - .deleteEmailToken(did, 'update_email') + await accntSrvce.deleteEmailToken(did, 'update_email') + } + if (user.email !== email) { + await accntSrvce.updateEmail(did, email) } - await dbTxn.db - .updateTable('user_account') - .set({ email, emailConfirmedAt: null }) - .where('did', '=', did) - .execute() }) }, }) diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 59115c9b6e5..a2f85c63998 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -200,7 +200,7 @@ export class AccountService { async updateEmail(did: string, email: string) { await this.db.db .updateTable('user_account') - .set({ email: email.toLowerCase() }) + .set({ email: email.toLowerCase(), emailConfirmedAt: null }) .where('did', '=', did) .executeTakeFirst() } From 96d0f01624694bf78ef82d0170db7a13f16c2389 Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 18:18:31 -0500 Subject: [PATCH 08/18] tests --- lexicons/com/atproto/server/confirmEmail.json | 7 +- .../com/atproto/server/createSession.json | 3 +- lexicons/com/atproto/server/getSession.json | 3 +- lexicons/com/atproto/server/updateEmail.json | 6 +- packages/api/src/client/lexicons.ts | 15 ++ .../types/com/atproto/server/confirmEmail.ts | 14 ++ .../types/com/atproto/server/createSession.ts | 1 + .../types/com/atproto/server/getSession.ts | 1 + .../types/com/atproto/server/updateEmail.ts | 7 + packages/bsky/src/lexicon/lexicons.ts | 15 ++ .../types/com/atproto/server/confirmEmail.ts | 2 +- .../types/com/atproto/server/createSession.ts | 1 + .../types/com/atproto/server/getSession.ts | 1 + .../types/com/atproto/server/updateEmail.ts | 2 +- .../api/com/atproto/server/confirmEmail.ts | 11 +- .../api/com/atproto/server/createSession.ts | 1 + .../src/api/com/atproto/server/getSession.ts | 7 +- packages/pds/src/lexicon/lexicons.ts | 15 ++ .../types/com/atproto/server/confirmEmail.ts | 2 +- .../types/com/atproto/server/createSession.ts | 1 + .../types/com/atproto/server/getSession.ts | 1 + .../types/com/atproto/server/updateEmail.ts | 2 +- packages/pds/tests/email-confirmation.test.ts | 177 ++++++++++++++++++ 23 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 packages/pds/tests/email-confirmation.test.ts diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json index ad064cf64b9..12d6fc57b57 100644 --- a/lexicons/com/atproto/server/confirmEmail.json +++ b/lexicons/com/atproto/server/confirmEmail.json @@ -16,7 +16,12 @@ } } }, - "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] + "errors": [ + { "name": "UserNotFound" }, + { "name": "ExpiredToken" }, + { "name": "InvalidToken" }, + { "name": "InvalidEmail" } + ] } } } diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json index fc416ddabae..7d877cec91c 100644 --- a/lexicons/com/atproto/server/createSession.json +++ b/lexicons/com/atproto/server/createSession.json @@ -29,7 +29,8 @@ "refreshJwt": { "type": "string" }, "handle": { "type": "string", "format": "handle" }, "did": { "type": "string", "format": "did" }, - "email": { "type": "string" } + "email": { "type": "string" }, + "emailConfirmed": { "type": "boolean" } } } }, diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json index 55b129be3df..5d2b6ee82fd 100644 --- a/lexicons/com/atproto/server/getSession.json +++ b/lexicons/com/atproto/server/getSession.json @@ -13,7 +13,8 @@ "properties": { "handle": { "type": "string", "format": "handle" }, "did": { "type": "string", "format": "did" }, - "email": { "type": "string" } + "email": { "type": "string" }, + "emailConfirmed": {"type": "boolean" } } } } diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json index 2cd84a3e5bc..276baf42670 100644 --- a/lexicons/com/atproto/server/updateEmail.json +++ b/lexicons/com/atproto/server/updateEmail.json @@ -19,7 +19,11 @@ } } }, - "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] + "errors": [ + { "name": "ExpiredToken" }, + { "name": "InvalidToken" }, + { "name": "TokenRequired" } + ] } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index ff72487df77..6520cfc6fd0 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2262,12 +2262,18 @@ export const schemaDict = { }, }, errors: [ + { + name: 'UserNotFound', + }, { name: 'ExpiredToken', }, { name: 'InvalidToken', }, + { + name: 'InvalidEmail', + }, ], }, }, @@ -2556,6 +2562,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -2786,6 +2795,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -3025,6 +3037,9 @@ export const schemaDict = { { name: 'InvalidToken', }, + { + name: 'TokenRequired', + }, ], }, }, diff --git a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts index 64a9d6811bb..cacb946a28c 100644 --- a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts +++ b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts @@ -26,6 +26,12 @@ export interface Response { headers: Headers } +export class UserNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + export class ExpiredTokenError extends XRPCError { constructor(src: XRPCError) { super(src.status, src.error, src.message, src.headers) @@ -38,10 +44,18 @@ export class InvalidTokenError extends XRPCError { } } +export class InvalidEmailError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + export function toKnownErr(e: any) { if (e instanceof XRPCError) { + if (e.error === 'UserNotFound') return new UserNotFoundError(e) if (e.error === 'ExpiredToken') return new ExpiredTokenError(e) if (e.error === 'InvalidToken') return new InvalidTokenError(e) + if (e.error === 'InvalidEmail') return new InvalidEmailError(e) } return e } diff --git a/packages/api/src/client/types/com/atproto/server/createSession.ts b/packages/api/src/client/types/com/atproto/server/createSession.ts index d86f2aef1d4..08d2bcd6225 100644 --- a/packages/api/src/client/types/com/atproto/server/createSession.ts +++ b/packages/api/src/client/types/com/atproto/server/createSession.ts @@ -22,6 +22,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/server/getSession.ts b/packages/api/src/client/types/com/atproto/server/getSession.ts index c15836dfb77..91d51860982 100644 --- a/packages/api/src/client/types/com/atproto/server/getSession.ts +++ b/packages/api/src/client/types/com/atproto/server/getSession.ts @@ -15,6 +15,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts index 6b7c476d0a9..b16864fe684 100644 --- a/packages/api/src/client/types/com/atproto/server/updateEmail.ts +++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts @@ -39,10 +39,17 @@ export class InvalidTokenError extends XRPCError { } } +export class TokenRequiredError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + export function toKnownErr(e: any) { if (e instanceof XRPCError) { if (e.error === 'ExpiredToken') return new ExpiredTokenError(e) if (e.error === 'InvalidToken') return new InvalidTokenError(e) + if (e.error === 'TokenRequired') return new TokenRequiredError(e) } return e } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index ff72487df77..6520cfc6fd0 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2262,12 +2262,18 @@ export const schemaDict = { }, }, errors: [ + { + name: 'UserNotFound', + }, { name: 'ExpiredToken', }, { name: 'InvalidToken', }, + { + name: 'InvalidEmail', + }, ], }, }, @@ -2556,6 +2562,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -2786,6 +2795,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -3025,6 +3037,9 @@ export const schemaDict = { { name: 'InvalidToken', }, + { + name: 'TokenRequired', + }, ], }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts index 6d2d89db7d0..b92a1cb66ca 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -24,7 +24,7 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'ExpiredToken' | 'InvalidToken' + error?: 'UserNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail' } export type HandlerOutput = HandlerError | void diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts index b836551f301..037900346a1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts @@ -23,6 +23,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts index 388fb5eae9d..7f066a500bf 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts @@ -16,6 +16,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts index c2c99ea01c8..17025f45a98 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -25,7 +25,7 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'ExpiredToken' | 'InvalidToken' + error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired' } export type HandlerOutput = HandlerError | void diff --git a/packages/pds/src/api/com/atproto/server/confirmEmail.ts b/packages/pds/src/api/com/atproto/server/confirmEmail.ts index 3e42931ae0f..b46402b1079 100644 --- a/packages/pds/src/api/com/atproto/server/confirmEmail.ts +++ b/packages/pds/src/api/com/atproto/server/confirmEmail.ts @@ -1,13 +1,22 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.confirmEmail({ auth: ctx.accessVerifierCheckTakedown, handler: async ({ auth, input }) => { const did = auth.credentials.did - const { token } = input.body + const { token, email } = input.body + const user = await ctx.services.account(ctx.db).getAccount(did) + if (!user) { + throw new InvalidRequestError('user not found') + } + + if (user.email !== email.toLowerCase()) { + throw new InvalidRequestError('invalid email', 'InvalidEmail') + } await ctx.services .account(ctx.db) .assertValidToken(did, 'confirm_email', token) diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index aee0063d86c..50791f23c7e 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -68,6 +68,7 @@ export default function (server: Server, ctx: AppContext) { did: user.did, handle: user.handle, email: user.email, + emailConfirmed: !!user.emailConfirmedAt, accessJwt: access.jwt, refreshJwt: refresh.jwt, }, diff --git a/packages/pds/src/api/com/atproto/server/getSession.ts b/packages/pds/src/api/com/atproto/server/getSession.ts index bfcccc97657..eb33180e6ff 100644 --- a/packages/pds/src/api/com/atproto/server/getSession.ts +++ b/packages/pds/src/api/com/atproto/server/getSession.ts @@ -15,7 +15,12 @@ export default function (server: Server, ctx: AppContext) { } return { encoding: 'application/json', - body: { handle: user.handle, did: user.did, email: user.email }, + body: { + handle: user.handle, + did: user.did, + email: user.email, + emailConfirmed: !!user.emailConfirmedAt, + }, } }, }) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index ff72487df77..6520cfc6fd0 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2262,12 +2262,18 @@ export const schemaDict = { }, }, errors: [ + { + name: 'UserNotFound', + }, { name: 'ExpiredToken', }, { name: 'InvalidToken', }, + { + name: 'InvalidEmail', + }, ], }, }, @@ -2556,6 +2562,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -2786,6 +2795,9 @@ export const schemaDict = { email: { type: 'string', }, + emailConfirmed: { + type: 'boolean', + }, }, }, }, @@ -3025,6 +3037,9 @@ export const schemaDict = { { name: 'InvalidToken', }, + { + name: 'TokenRequired', + }, ], }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts index 6d2d89db7d0..b92a1cb66ca 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -24,7 +24,7 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'ExpiredToken' | 'InvalidToken' + error?: 'UserNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail' } export type HandlerOutput = HandlerError | void diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts index b836551f301..037900346a1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts @@ -23,6 +23,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts index 388fb5eae9d..7f066a500bf 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts @@ -16,6 +16,7 @@ export interface OutputSchema { handle: string did: string email?: string + emailConfirmed?: boolean [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts index c2c99ea01c8..17025f45a98 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -25,7 +25,7 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'ExpiredToken' | 'InvalidToken' + error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired' } export type HandlerOutput = HandlerError | void diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts new file mode 100644 index 00000000000..9c283c8a2ee --- /dev/null +++ b/packages/pds/tests/email-confirmation.test.ts @@ -0,0 +1,177 @@ +import { once, EventEmitter } from 'events' +import Mail from 'nodemailer/lib/mailer' +import AtpAgent from '@atproto/api' +import { SeedClient } from './seeds/client' +import userSeed from './seeds/users' +import { ServerMailer } from '../src/mailer' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { + ComAtprotoServerConfirmEmail, + ComAtprotoServerUpdateEmail, +} from '@atproto/api' + +describe('email confirmation', () => { + let network: TestNetworkNoAppView + let agent: AtpAgent + let sc: SeedClient + + let mailer: ServerMailer + const mailCatcher = new EventEmitter() + let _origSendMail + + let alice + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'email_confirmation', + }) + mailer = network.pds.ctx.mailer + agent = network.pds.getClient() + sc = new SeedClient(agent) + await userSeed(sc) + alice = sc.accounts[sc.dids.alice] + + // Catch emails for use in tests + _origSendMail = mailer.transporter.sendMail + mailer.transporter.sendMail = async (opts) => { + const result = await _origSendMail.call(mailer.transporter, opts) + mailCatcher.emit('mail', opts) + return result + } + }) + + afterAll(async () => { + mailer.transporter.sendMail = _origSendMail + await network.close() + }) + + const getMailFrom = async (promise): Promise => { + const result = await Promise.all([once(mailCatcher, 'mail'), promise]) + return result[0][0] + } + + const getTokenFromMail = (mail: Mail.Options) => + mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5}) { + const res = await agent.api.com.atproto.server.requestEmailUpdate( + undefined, + { headers: sc.getHeaders(alice.did) }, + ) + expect(res.data.tokenRequired).toBe(false) + + await agent.api.com.atproto.server.updateEmail( + { + email: 'new-alice@example.com', + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) + const session = await agent.api.com.atproto.server.getSession( + {}, + { headers: sc.getHeaders(alice.did) }, + ) + expect(session.data.email).toEqual('new-alice@example.com') + expect(session.data.emailConfirmed).toEqual(false) + alice.email = session.data.email + }) + + let confirmToken + + it('requests email confirmation', async () => { + const mail = await getMailFrom( + agent.api.com.atproto.server.requestEmailConfirmation(undefined, { + headers: sc.getHeaders(alice.did), + }), + ) + expect(mail.to).toEqual(alice.email) + expect(mail.html).toEqual('Confirm your Bluesky email') + confirmToken = getTokenFromMail(mail) + expect(confirmToken).toBeDefined() + }) + + it('fails email confirmation with a bad token', async () => { + const attempt = agent.api.com.atproto.server.confirmEmail({ + email: 'new-alice@example.com', + token: '123456', + }) + await expect(attempt).rejects.toThrow( + ComAtprotoServerConfirmEmail.InvalidTokenError, + ) + }) + + it('fails email confirmation with a bad token', async () => { + const attempt = agent.api.com.atproto.server.confirmEmail({ + email: 'fake-alice@example.com', + token: confirmToken, + }) + await expect(attempt).rejects.toThrow( + ComAtprotoServerConfirmEmail.InvalidEmailError, + ) + }) + + it('confirms email', async () => { + await agent.api.com.atproto.server.confirmEmail({ + email: 'new-alice@example.com', + token: confirmToken, + }) + const session = await agent.api.com.atproto.server.getSession( + {}, + { headers: sc.getHeaders(alice.did) }, + ) + expect(session.data.emailConfirmed).toBe(true) + }) + + it('disallows email update without token when verified', async () => { + const attempt = agent.api.com.atproto.server.updateEmail( + { + email: 'new-alice-2@example.com', + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) + await expect(attempt).rejects.toThrow( + ComAtprotoServerUpdateEmail.TokenRequiredError, + ) + }) + + let updateToken + + it('requests email update', async () => { + const mail = await getMailFrom(async () => { + const res = await agent.api.com.atproto.server.requestEmailUpdate( + undefined, + { + headers: sc.getHeaders(alice.did), + }, + ) + expect(res.data.tokenRequired).toBe(true) + }) + expect(mail.to).toEqual(alice.email) + expect(mail.html).toEqual('Update your Bluesky email') + updateToken = getTokenFromMail(mail) + expect(updateToken).toBeDefined() + }) + + it('fails email update with a bad token', async () => { + const attempt = agent.api.com.atproto.server.updateEmail({ + email: 'new-alice-2@example.com', + token: '123456', + }) + await expect(attempt).rejects.toThrow( + ComAtprotoServerUpdateEmail.InvalidTokenError, + ) + }) + + it('updates email', async () => { + await agent.api.com.atproto.server.updateEmail({ + email: 'new-alice-2@example.com', + token: updateToken, + }) + + const session = await agent.api.com.atproto.server.getSession( + {}, + { headers: sc.getHeaders(alice.did) }, + ) + expect(session.data.email).toBe('new-alice-2@example.com') + expect(session.data.emailConfirmed).toBe(false) + }) +}) From 81a3f56571f34f21a6394e9d6d92e58d83715a3a Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 18:41:24 -0500 Subject: [PATCH 09/18] tidy & bugfixes --- .../com/atproto/server/requestEmailUpdate.ts | 2 +- .../src/api/com/atproto/server/updateEmail.ts | 4 +- .../20230908T224408678Z-email-tokens.ts | 9 +++ packages/pds/tests/email-confirmation.test.ts | 72 ++++++++++++------- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts index e38b69a6d7e..bcc65303f41 100644 --- a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts +++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('user not found') } - const tokenRequired = user.emailConfirmedAt !== null + const tokenRequired = !!user.emailConfirmedAt if (tokenRequired) { const token = await ctx.services .account(ctx.db) diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts index 095b89ce2b0..c87ffa16b82 100644 --- a/packages/pds/src/api/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts @@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('user not found') } // require valid token - if (user.emailConfirmedAt !== null) { + if (user.emailConfirmedAt) { if (!token) { throw new InvalidRequestError( 'confirmation token required', @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { } await ctx.services .account(ctx.db) - .assertValidToken(did, 'confirm_email', token) + .assertValidToken(did, 'update_email', token) } await ctx.db.transaction(async (dbTxn) => { diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts index f32eef3f54e..d775f6f0a9c 100644 --- a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts +++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts @@ -11,8 +11,17 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('requestedAt', timestamp, (col) => col.notNull()) .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did']) .execute() + + await db.schema + .alterTable('user_account') + .addColumn('emailConfirmedAt', 'varchar') + .execute() } export async function down(db: Kysely): Promise { await db.schema.dropTable('email_token').execute() + await db.schema + .alterTable('user_account') + .dropColumn('emailConfirmedAt') + .execute() } diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts index 9c283c8a2ee..00d4acfc99d 100644 --- a/packages/pds/tests/email-confirmation.test.ts +++ b/packages/pds/tests/email-confirmation.test.ts @@ -53,6 +53,14 @@ describe('email confirmation', () => { const getTokenFromMail = (mail: Mail.Options) => mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5}) { + const session = await agent.api.com.atproto.server.getSession( + {}, + { headers: sc.getHeaders(alice.did) }, + ) + expect(session.data.emailConfirmed).toEqual(false) + }) + it('allows email update without token when unverified', async () => { const res = await agent.api.com.atproto.server.requestEmailUpdate( undefined, @@ -84,36 +92,45 @@ describe('email confirmation', () => { }), ) expect(mail.to).toEqual(alice.email) - expect(mail.html).toEqual('Confirm your Bluesky email') + expect(mail.html).toContain('Confirm your Bluesky email') confirmToken = getTokenFromMail(mail) expect(confirmToken).toBeDefined() }) it('fails email confirmation with a bad token', async () => { - const attempt = agent.api.com.atproto.server.confirmEmail({ - email: 'new-alice@example.com', - token: '123456', - }) + const attempt = agent.api.com.atproto.server.confirmEmail( + { + email: 'new-alice@example.com', + token: '123456', + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) await expect(attempt).rejects.toThrow( ComAtprotoServerConfirmEmail.InvalidTokenError, ) }) it('fails email confirmation with a bad token', async () => { - const attempt = agent.api.com.atproto.server.confirmEmail({ - email: 'fake-alice@example.com', - token: confirmToken, - }) + const attempt = agent.api.com.atproto.server.confirmEmail( + { + email: 'fake-alice@example.com', + token: confirmToken, + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) await expect(attempt).rejects.toThrow( ComAtprotoServerConfirmEmail.InvalidEmailError, ) }) it('confirms email', async () => { - await agent.api.com.atproto.server.confirmEmail({ - email: 'new-alice@example.com', - token: confirmToken, - }) + await agent.api.com.atproto.server.confirmEmail( + { + email: 'new-alice@example.com', + token: confirmToken, + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) const session = await agent.api.com.atproto.server.getSession( {}, { headers: sc.getHeaders(alice.did) }, @@ -136,7 +153,7 @@ describe('email confirmation', () => { let updateToken it('requests email update', async () => { - const mail = await getMailFrom(async () => { + const reqUpdate = async () => { const res = await agent.api.com.atproto.server.requestEmailUpdate( undefined, { @@ -144,28 +161,35 @@ describe('email confirmation', () => { }, ) expect(res.data.tokenRequired).toBe(true) - }) + } + const mail = await getMailFrom(reqUpdate()) expect(mail.to).toEqual(alice.email) - expect(mail.html).toEqual('Update your Bluesky email') + expect(mail.html).toContain('Update your Bluesky email') updateToken = getTokenFromMail(mail) expect(updateToken).toBeDefined() }) it('fails email update with a bad token', async () => { - const attempt = agent.api.com.atproto.server.updateEmail({ - email: 'new-alice-2@example.com', - token: '123456', - }) + const attempt = agent.api.com.atproto.server.updateEmail( + { + email: 'new-alice-2@example.com', + token: '123456', + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) await expect(attempt).rejects.toThrow( ComAtprotoServerUpdateEmail.InvalidTokenError, ) }) it('updates email', async () => { - await agent.api.com.atproto.server.updateEmail({ - email: 'new-alice-2@example.com', - token: updateToken, - }) + await agent.api.com.atproto.server.updateEmail( + { + email: 'new-alice-2@example.com', + token: updateToken, + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) const session = await agent.api.com.atproto.server.getSession( {}, From e2f7a9a852795438f083bdbfb0a5157d9075781a Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 18:42:35 -0500 Subject: [PATCH 10/18] format --- lexicons/com/atproto/server/getSession.json | 2 +- lexicons/com/atproto/server/updateEmail.json | 2 +- packages/api/docs/labels.md | 54 +- .../api/docs/moderation-behaviors/posts.md | 529 +----------------- .../api/docs/moderation-behaviors/profiles.md | 188 +------ .../src/mailer/templates/confirm-email.hbs | 6 +- .../pds/src/mailer/templates/update-email.hbs | 6 +- 7 files changed, 63 insertions(+), 724 deletions(-) diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json index 5d2b6ee82fd..7ff5569eb1b 100644 --- a/lexicons/com/atproto/server/getSession.json +++ b/lexicons/com/atproto/server/getSession.json @@ -14,7 +14,7 @@ "handle": { "type": "string", "format": "handle" }, "did": { "type": "string", "format": "did" }, "email": { "type": "string" }, - "emailConfirmed": {"type": "boolean" } + "emailConfirmed": { "type": "boolean" } } } } diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json index 276baf42670..c51aef5dd68 100644 --- a/lexicons/com/atproto/server/updateEmail.json +++ b/lexicons/com/atproto/server/updateEmail.json @@ -12,7 +12,7 @@ "required": ["email"], "properties": { "email": { "type": "string" }, - "token": { + "token": { "type": "string", "description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed." } diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md index 9531df460c1..a2d8806b566 100644 --- a/packages/api/docs/labels.md +++ b/packages/api/docs/labels.md @@ -1,44 +1,44 @@ - # Labels - - This document is a reference for the labels used in the SDK. +# Labels - **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. +This document is a reference for the labels used in the SDK. - ## Key +**⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. - ### Label Preferences +## Key - The possible client interpretations for a label. +### Label Preferences - - ignore Do nothing with the label. - - warn Provide some form of warning on the content (see "On Warn" behavior). - - hide Remove the content from feeds and apply the warning when directly viewed. +The possible client interpretations for a label. - Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. +- ignore Do nothing with the label. +- warn Provide some form of warning on the content (see "On Warn" behavior). +- hide Remove the content from feeds and apply the warning when directly viewed. - ### Configurable? +Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. - Non-configurable labels cannot have their preference changed by the user. +### Configurable? - ### Flags +Non-configurable labels cannot have their preference changed by the user. - Additional behaviors which a label can adopt. +### Flags - - no-override The user cannot click through any covering of content created by the label. - - adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. +Additional behaviors which a label can adopt. - ### On Warn +- no-override The user cannot click through any covering of content created by the label. +- adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. - The kind of UI behavior used when a warning must be applied. +### On Warn - - blur Hide all of the content behind an interstitial. - - blur-media Hide only the media within the content (ie images) behind an interstitial. - - alert Display a descriptive warning but do not hide the content. - - null Do nothing. +The kind of UI behavior used when a warning must be applied. - ## Label Behaviors +- blur Hide all of the content behind an interstitial. +- blur-media Hide only the media within the content (ie images) behind an interstitial. +- alert Display a descriptive warning but do not hide the content. +- null Do nothing. + +## Label Behaviors @@ -267,7 +267,7 @@
- ## Label Group Descriptions +## Label Group Descriptions @@ -312,7 +312,7 @@
- ## Label Descriptions +## Label Descriptions @@ -535,4 +535,4 @@ on content
Misleading
The moderators believe this account is spreading misleading information.

-
\ No newline at end of file + diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md index ef3c6c7fc5d..5ddcf9ff602 100644 --- a/packages/api/docs/moderation-behaviors/posts.md +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -38,17 +38,12 @@ Key: - - - - - Imperative label ('!hide') on author profile @@ -56,7 +51,6 @@ Key: - 🚫 @@ -64,13 +58,9 @@ Key: - - - - Imperative label ('!hide') on author account @@ -86,13 +76,9 @@ Key: - - - - Imperative label ('!hide') on quoted post @@ -100,11 +86,9 @@ Key: - - 🚫 @@ -112,9 +96,6 @@ Key: - - - Imperative label ('!hide') on quoted author account @@ -122,11 +103,9 @@ Key: - - 🚫 @@ -134,9 +113,6 @@ Key: - - - Imperative label ('!no-promote') on post @@ -144,21 +120,15 @@ Key: - - - - - - Imperative label ('!no-promote') on author profile @@ -166,21 +136,15 @@ Key: - - - - - - Imperative label ('!no-promote') on author account @@ -188,21 +152,15 @@ Key: - - - - - - Imperative label ('!no-promote') on quoted post @@ -210,21 +168,15 @@ Key: - - - - - - Imperative label ('!no-promote') on quoted author account @@ -232,21 +184,15 @@ Key: - - - - - - Imperative label ('!warn') on post @@ -258,17 +204,12 @@ Key: - - - - - Imperative label ('!warn') on author profile @@ -276,7 +217,6 @@ Key: - ✋ @@ -284,13 +224,9 @@ Key: - - - - Imperative label ('!warn') on author account @@ -306,13 +242,9 @@ Key: - - - - Imperative label ('!warn') on quoted post @@ -320,11 +252,9 @@ Key: - - ✋ @@ -332,9 +262,6 @@ Key: - - - Imperative label ('!warn') on quoted author account @@ -342,11 +269,9 @@ Key: - - ✋ @@ -354,8 +279,6 @@ Key: - - ScenarioFilterContentAvatarEmbed Blur label ('intolerant') on post (hide) @@ -368,17 +291,12 @@ Key: - - - - - Blur label ('intolerant') on author profile (hide) @@ -386,7 +304,6 @@ Key: - ✋ @@ -394,13 +311,9 @@ Key: - - - - Blur label ('intolerant') on author account (hide) @@ -416,13 +329,9 @@ Key: - - - - Blur label ('intolerant') on quoted post (hide) @@ -430,11 +339,9 @@ Key: - - ✋ @@ -442,9 +349,6 @@ Key: - - - Blur label ('intolerant') on quoted author account (hide) @@ -452,11 +356,9 @@ Key: - - ✋ @@ -464,9 +366,6 @@ Key: - - - Blur label ('intolerant') on post (warn) @@ -478,17 +377,12 @@ Key: - - - - - Blur label ('intolerant') on author profile (warn) @@ -496,7 +390,6 @@ Key: - ✋ @@ -504,13 +397,9 @@ Key: - - - - Blur label ('intolerant') on author account (warn) @@ -526,13 +415,9 @@ Key: - - - - Blur label ('intolerant') on quoted post (warn) @@ -540,11 +425,9 @@ Key: - - ✋ @@ -552,9 +435,6 @@ Key: - - - Blur label ('intolerant') on quoted author account (warn) @@ -562,11 +442,9 @@ Key: - - ✋ @@ -574,9 +452,6 @@ Key: - - - Blur label ('intolerant') on post (ignore) @@ -584,21 +459,15 @@ Key: - - - - - - Blur label ('intolerant') on author profile (ignore) @@ -606,21 +475,15 @@ Key: - - - - - - Blur label ('intolerant') on author account (ignore) @@ -628,21 +491,15 @@ Key: - - - - - - Blur label ('intolerant') on quoted post (ignore) @@ -650,21 +507,15 @@ Key: - - - - - - Blur label ('intolerant') on quoted author account (ignore) @@ -672,20 +523,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Blur-media label ('porn') on post (hide) @@ -694,11 +540,9 @@ Key: - - ✋ @@ -706,9 +550,6 @@ Key: - - - Blur-media label ('porn') on author profile (hide) @@ -716,7 +557,6 @@ Key: - ✋ @@ -724,13 +564,9 @@ Key: - - - - Blur-media label ('porn') on author account (hide) @@ -738,7 +574,6 @@ Key: - ✋ @@ -750,9 +585,6 @@ Key: - - - Blur-media label ('porn') on quoted post (hide) @@ -760,11 +592,9 @@ Key: - - ✋ @@ -772,9 +602,6 @@ Key: - - - Blur-media label ('porn') on quoted author account (hide) @@ -782,21 +609,15 @@ Key: - - - - - - Blur-media label ('porn') on post (warn) @@ -804,11 +625,9 @@ Key: - - ✋ @@ -816,9 +635,6 @@ Key: - - - Blur-media label ('porn') on author profile (warn) @@ -826,7 +642,6 @@ Key: - ✋ @@ -834,13 +649,9 @@ Key: - - - - Blur-media label ('porn') on author account (warn) @@ -848,7 +659,6 @@ Key: - ✋ @@ -860,9 +670,6 @@ Key: - - - Blur-media label ('porn') on quoted post (warn) @@ -870,11 +677,9 @@ Key: - - ✋ @@ -882,9 +687,6 @@ Key: - - - Blur-media label ('porn') on quoted author account (warn) @@ -892,21 +694,15 @@ Key: - - - - - - Blur-media label ('porn') on post (ignore) @@ -914,21 +710,15 @@ Key: - - - - - - Blur-media label ('porn') on author profile (ignore) @@ -936,21 +726,15 @@ Key: - - - - - - Blur-media label ('porn') on author account (ignore) @@ -958,21 +742,15 @@ Key: - - - - - - Blur-media label ('porn') on quoted post (ignore) @@ -980,21 +758,15 @@ Key: - - - - - - Blur-media label ('porn') on quoted author account (ignore) @@ -1002,20 +774,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Notice label ('scam') on post (hide) @@ -1025,20 +792,16 @@ Key: 🪧 + - - - - - Notice label ('scam') on author profile (hide) @@ -1046,21 +809,17 @@ Key: - 🪧 + - - - - Notice label ('scam') on author account (hide) @@ -1069,20 +828,18 @@ Key: 🪧 + 🪧 + - - - - Notice label ('scam') on quoted post (hide) @@ -1090,21 +847,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on quoted author account (hide) @@ -1112,21 +865,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on post (warn) @@ -1135,20 +884,16 @@ Key: 🪧 + - - - - - Notice label ('scam') on author profile (warn) @@ -1156,21 +901,17 @@ Key: - 🪧 + - - - - Notice label ('scam') on author account (warn) @@ -1179,20 +920,18 @@ Key: 🪧 + 🪧 + - - - - Notice label ('scam') on quoted post (warn) @@ -1200,21 +939,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on quoted author account (warn) @@ -1222,21 +957,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on post (ignore) @@ -1244,21 +975,15 @@ Key: - - - - - - Notice label ('scam') on author profile (ignore) @@ -1266,21 +991,15 @@ Key: - - - - - - Notice label ('scam') on author account (ignore) @@ -1288,21 +1007,15 @@ Key: - - - - - - Notice label ('scam') on quoted post (ignore) @@ -1310,21 +1023,15 @@ Key: - - - - - - Notice label ('scam') on quoted author account (ignore) @@ -1332,20 +1039,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Adult-only label on post when adult content is disabled @@ -1354,11 +1056,9 @@ Key: - - 🚫 @@ -1366,9 +1066,6 @@ Key: - - - Adult-only label on author profile when adult content is disabled @@ -1376,7 +1073,6 @@ Key: - 🚫 @@ -1384,13 +1080,9 @@ Key: - - - - Adult-only label on author account when adult content is disabled @@ -1398,7 +1090,6 @@ Key: - 🚫 @@ -1410,9 +1101,6 @@ Key: - - - Adult-only label on quoted post when adult content is disabled @@ -1420,11 +1108,9 @@ Key: - - 🚫 @@ -1432,9 +1118,6 @@ Key: - - - Adult-only label on quoted author account when adult content is disabled @@ -1442,20 +1125,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Self-post: Imperative label ('!hide') on post @@ -1468,17 +1146,12 @@ Key: - - - - - Self-post: Imperative label ('!hide') on author profile @@ -1486,21 +1159,15 @@ Key: - - - - - - Self-post: Imperative label ('!hide') on author account @@ -1508,21 +1175,15 @@ Key: - - - - - - Self-post: Imperative label ('!hide') on quoted post @@ -1530,11 +1191,9 @@ Key: - - ✋ @@ -1542,9 +1201,6 @@ Key: - - - Self-post: Imperative label ('!hide') on quoted author account @@ -1552,21 +1208,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on post @@ -1578,17 +1228,12 @@ Key: - - - - - Self-post: Imperative label ('!warn') on author profile @@ -1596,21 +1241,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on author account @@ -1618,21 +1257,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on quoted post @@ -1640,11 +1273,9 @@ Key: - - ✋ @@ -1652,9 +1283,6 @@ Key: - - - Self-post: Imperative label ('!warn') on quoted author account @@ -1662,21 +1290,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on post (hide) @@ -1684,11 +1306,9 @@ Key: - - ✋ @@ -1696,9 +1316,6 @@ Key: - - - Self-post: Blur-media label ('porn') on author profile (hide) @@ -1706,21 +1323,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on author account (hide) @@ -1728,21 +1339,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on quoted post (hide) @@ -1750,11 +1355,9 @@ Key: - - ✋ @@ -1762,9 +1365,6 @@ Key: - - - Self-post: Blur-media label ('porn') on quoted author account (hide) @@ -1772,21 +1372,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on post (warn) @@ -1794,11 +1388,9 @@ Key: - - ✋ @@ -1806,9 +1398,6 @@ Key: - - - Self-post: Blur-media label ('porn') on author profile (warn) @@ -1816,21 +1405,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on author account (warn) @@ -1838,21 +1421,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on quoted post (warn) @@ -1860,11 +1437,9 @@ Key: - - ✋ @@ -1872,9 +1447,6 @@ Key: - - - Self-post: Blur-media label ('porn') on quoted author account (warn) @@ -1882,20 +1454,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Post with blocked author @@ -1912,13 +1479,9 @@ Key: - - - - Post with blocked quoted author @@ -1926,11 +1489,9 @@ Key: - - 🚫 @@ -1938,9 +1499,6 @@ Key: - - - Post with author blocking user @@ -1956,13 +1514,9 @@ Key: - - - - Post with quoted author blocking user @@ -1970,11 +1524,9 @@ Key: - - 🚫 @@ -1982,9 +1534,6 @@ Key: - - - Post with muted author @@ -1996,17 +1545,12 @@ Key: - - - - - Post with muted quoted author @@ -2014,11 +1558,9 @@ Key: - - ✋ @@ -2026,9 +1568,6 @@ Key: - - - Post with muted-by-list author @@ -2040,17 +1579,12 @@ Key: - - - - - Post with muted-by-list quoted author @@ -2058,11 +1592,9 @@ Key: - - ✋ @@ -2070,8 +1602,6 @@ Key: - - ScenarioFilterContentAvatarEmbed Prioritization: post with blocking & blocked-by author @@ -2088,13 +1618,9 @@ Key: - - - - Prioritization: post with blocking & blocked-by quoted author @@ -2102,11 +1628,9 @@ Key: - - 🚫 @@ -2114,9 +1638,6 @@ Key: - - - Prioritization: '!hide' label on post by blocked user @@ -2132,13 +1653,9 @@ Key: - - - - Prioritization: '!hide' label on quoted post, post by blocked user @@ -2158,9 +1675,6 @@ Key: - - - Prioritization: '!hide' and 'intolerant' labels on post (hide) @@ -2172,17 +1686,12 @@ Key: - - - - - Prioritization: '!warn' and 'intolerant' labels on post (hide) @@ -2194,17 +1703,12 @@ Key: - - - - - Prioritization: '!hide' and 'porn' labels on post (hide) @@ -2216,17 +1720,12 @@ Key: - - - - - Prioritization: '!warn' and 'porn' labels on post (hide) @@ -2234,11 +1733,9 @@ Key: - - ✋ @@ -2246,4 +1743,4 @@ Key: - \ No newline at end of file + diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md index 3d2f9af96b3..b8d7c94ce91 100644 --- a/packages/api/docs/moderation-behaviors/profiles.md +++ b/packages/api/docs/moderation-behaviors/profiles.md @@ -38,7 +38,6 @@ Key: - 🚫 @@ -46,9 +45,6 @@ Key: - - - Imperative label ('!hide') on profile @@ -56,7 +52,6 @@ Key: - 🚫 @@ -68,9 +63,6 @@ Key: - - - Imperative label ('!no-promote') on account @@ -78,21 +70,15 @@ Key: - - - - - - Imperative label ('!no-promote') on profile @@ -100,21 +86,15 @@ Key: - - - - - - Imperative label ('!warn') on account @@ -126,7 +106,6 @@ Key: - ✋ @@ -134,9 +113,6 @@ Key: - - - Imperative label ('!warn') on profile @@ -144,7 +120,6 @@ Key: - ✋ @@ -156,8 +131,6 @@ Key: - - ScenarioFilterAccountProfileAvatar Blur label ('intolerant') on account (hide) @@ -170,7 +143,6 @@ Key: - ✋ @@ -178,9 +150,6 @@ Key: - - - Blur label ('intolerant') on profile (hide) @@ -188,7 +157,6 @@ Key: - ✋ @@ -200,9 +168,6 @@ Key: - - - Blur label ('intolerant') on account (warn) @@ -214,7 +179,6 @@ Key: - ✋ @@ -222,9 +186,6 @@ Key: - - - Blur label ('intolerant') on profile (warn) @@ -232,7 +193,6 @@ Key: - ✋ @@ -244,9 +204,6 @@ Key: - - - Blur label ('intolerant') on account (ignore) @@ -254,21 +211,15 @@ Key: - - - - - - Blur label ('intolerant') on profile (ignore) @@ -276,20 +227,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Blur-media label ('porn') on account (hide) @@ -302,7 +248,6 @@ Key: - ✋ @@ -310,9 +255,6 @@ Key: - - - Blur-media label ('porn') on profile (hide) @@ -320,11 +262,9 @@ Key: - - ✋ @@ -332,9 +272,6 @@ Key: - - - Blur-media label ('porn') on account (warn) @@ -346,7 +283,6 @@ Key: - ✋ @@ -354,9 +290,6 @@ Key: - - - Blur-media label ('porn') on profile (warn) @@ -364,11 +297,9 @@ Key: - - ✋ @@ -376,9 +307,6 @@ Key: - - - Blur-media label ('porn') on account (ignore) @@ -386,21 +314,15 @@ Key: - - - - - - Blur-media label ('porn') on profile (ignore) @@ -408,20 +330,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Notice label ('scam') on account (hide) @@ -431,20 +348,18 @@ Key: 🪧 + - 🪧 + - - - Notice label ('scam') on profile (hide) @@ -452,21 +367,19 @@ Key: - 🪧 + 🪧 + - - - Notice label ('scam') on account (warn) @@ -475,20 +388,18 @@ Key: 🪧 + - 🪧 + - - - Notice label ('scam') on profile (warn) @@ -496,21 +407,19 @@ Key: - 🪧 + 🪧 + - - - Notice label ('scam') on account (ignore) @@ -518,21 +427,15 @@ Key: - - - - - - Notice label ('scam') on profile (ignore) @@ -540,20 +443,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Adult-only label on account when adult content is disabled @@ -566,7 +464,6 @@ Key: - 🚫 @@ -574,9 +471,6 @@ Key: - - - Adult-only label on profile when adult content is disabled @@ -584,11 +478,9 @@ Key: - - 🚫 @@ -596,8 +488,6 @@ Key: - - ScenarioFilterAccountProfileAvatar Self-profile: !hide on account @@ -607,20 +497,18 @@ Key: 🪧 + - 🪧 + - - - Self-profile: !hide on profile @@ -628,20 +516,19 @@ Key: - 🪧 + 🪧 + - - ScenarioFilterAccountProfileAvatar Mute/block: Blocking user @@ -650,11 +537,9 @@ Key: - - 🚫 @@ -662,9 +547,6 @@ Key: - - - Mute/block: Blocked by user @@ -672,11 +554,9 @@ Key: - - 🚫 @@ -684,9 +564,6 @@ Key: - - - Mute/block: Muted user @@ -694,21 +571,15 @@ Key: - - - - - - Mute/block: Muted-by-list user @@ -716,20 +587,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Prioritization: blocking & blocked-by user @@ -738,11 +604,9 @@ Key: - - 🚫 @@ -750,9 +614,6 @@ Key: - - - Prioritization: '!hide' label on account of blocked user @@ -764,7 +625,6 @@ Key: - 🚫 @@ -772,9 +632,6 @@ Key: - - - Prioritization: '!hide' and 'intolerant' labels on account (hide) @@ -786,7 +643,6 @@ Key: - 🚫 @@ -794,9 +650,6 @@ Key: - - - Prioritization: '!warn' and 'intolerant' labels on account (hide) @@ -808,7 +661,6 @@ Key: - ✋ @@ -816,9 +668,6 @@ Key: - - - Prioritization: '!warn' and 'porn' labels on account (hide) @@ -830,7 +679,6 @@ Key: - ✋ @@ -838,9 +686,6 @@ Key: - - - Prioritization: intolerant label on account (hide) and scam label on profile (warn) @@ -853,6 +698,7 @@ Key: 🪧 + ✋ @@ -860,9 +706,6 @@ Key: - - - Prioritization: !hide on account, !warn on profile @@ -882,9 +725,6 @@ Key: - - - Prioritization: !warn on account, !hide on profile @@ -904,4 +744,4 @@ Key: - \ No newline at end of file + diff --git a/packages/pds/src/mailer/templates/confirm-email.hbs b/packages/pds/src/mailer/templates/confirm-email.hbs index cf0f123b0f6..ee062a40e07 100644 --- a/packages/pds/src/mailer/templates/confirm-email.hbs +++ b/packages/pds/src/mailer/templates/confirm-email.hbs @@ -326,9 +326,9 @@ class="text-gray-700" style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;" align="left" - >To confirm this email for - your account, please enter the - above code in the app.

+ >To confirm this email for your + account, please enter the above + code in the app.

diff --git a/packages/pds/src/mailer/templates/update-email.hbs b/packages/pds/src/mailer/templates/update-email.hbs index a7c22fd5ca6..f49947125be 100644 --- a/packages/pds/src/mailer/templates/update-email.hbs +++ b/packages/pds/src/mailer/templates/update-email.hbs @@ -326,8 +326,10 @@ class="text-gray-700" style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;" align="left" - >To the email for your account, please enter the - above code alongside your new email in the app.

+ >To the email for your account, + please enter the above code + alongside your new email in the + app.

From b796ad2f566e4f2459672233d5f8da34068fe958 Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 18:46:56 -0500 Subject: [PATCH 11/18] fix api test --- packages/api/tests/agent.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts index 2444e7e0c61..cb8989724ef 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/agent.test.ts @@ -56,6 +56,7 @@ describe('agent', () => { did: res.data.did, handle: res.data.handle, email: 'user1@test.com', + emailConfirmed: false, }) expect(events.length).toEqual(1) @@ -100,6 +101,7 @@ describe('agent', () => { did: res1.data.did, handle: res1.data.handle, email, + emailConfirmed: false, }) expect(events.length).toEqual(2) @@ -142,6 +144,7 @@ describe('agent', () => { did: res1.data.did, handle: res1.data.handle, email: res1.data.email, + emailConfirmed: false, }) expect(events.length).toEqual(2) From b2232712caeaddb89b88cb56b8bc3407c58348bd Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 18:48:27 -0500 Subject: [PATCH 12/18] fix auth test --- packages/pds/tests/auth.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index c1883d5a7f7..ae78f3d5619 100644 --- a/packages/pds/tests/auth.test.ts +++ b/packages/pds/tests/auth.test.ts @@ -68,6 +68,7 @@ describe('auth', () => { did: account.did, handle: account.handle, email, + emailConfirmed: false, }) // Valid refresh token const nextSession = await refreshSession(account.refreshJwt) @@ -96,6 +97,7 @@ describe('auth', () => { did: session.did, handle: session.handle, email, + emailConfirmed: false, }) // Valid refresh token const nextSession = await refreshSession(session.refreshJwt) @@ -139,6 +141,7 @@ describe('auth', () => { did: session.did, handle: session.handle, email, + emailConfirmed: false, }) // Valid refresh token const nextSession = await refreshSession(session.refreshJwt) From 49b9c23eb0da9d0fee78f0ae4bdea047d0c2895f Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 8 Sep 2023 19:27:21 -0500 Subject: [PATCH 13/18] impl --- .../api/com/atproto/server/deleteAccount.ts | 66 ++---------------- .../atproto/server/requestAccountDelete.ts | 13 +--- .../atproto/server/requestPasswordReset.ts | 14 +--- .../api/com/atproto/server/resetPassword.ts | 68 ++----------------- packages/pds/src/db/database-schema.ts | 2 - .../20230908T224408678Z-email-tokens.ts | 33 +++++++++ .../pds/src/db/tables/delete-account-token.ts | 9 --- packages/pds/src/db/tables/user-account.ts | 2 - packages/pds/src/services/account/index.ts | 23 ++++++- packages/pds/tests/account.test.ts | 17 +++-- 10 files changed, 82 insertions(+), 165 deletions(-) delete mode 100644 packages/pds/src/db/tables/delete-account-token.ts diff --git a/packages/pds/src/api/com/atproto/server/deleteAccount.ts b/packages/pds/src/api/com/atproto/server/deleteAccount.ts index 9ebfcfa7fdf..4d12edb1b32 100644 --- a/packages/pds/src/api/com/atproto/server/deleteAccount.ts +++ b/packages/pds/src/api/com/atproto/server/deleteAccount.ts @@ -2,7 +2,6 @@ import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' import AppContext from '../../../../context' -import Database from '../../../../db' import { MINUTE } from '@atproto/common' const REASON_ACCT_DELETION = 'ACCOUNT DELETION' @@ -22,40 +21,18 @@ export default function (server: Server, ctx: AppContext) { throw new AuthRequiredError('Invalid did or password') } - const tokenInfo = await ctx.db.db - .selectFrom('did_handle') - .innerJoin( - 'delete_account_token as token', - 'token.did', - 'did_handle.did', - ) - .where('did_handle.did', '=', did) - .where('token.token', '=', token.toUpperCase()) - .select([ - 'token.token as token', - 'token.requestedAt as requestedAt', - 'token.did as did', - ]) - .executeTakeFirst() - - if (!tokenInfo) { - return createInvalidTokenError() - } + await ctx.services + .account(ctx.db) + .assertValidToken(did, 'delete_account', token) const now = new Date() - const requestedAt = new Date(tokenInfo.requestedAt) - const expiresAt = new Date(requestedAt.getTime() + 15 * minsToMs) - if (now > expiresAt) { - await removeDeleteToken(ctx.db, tokenInfo.did) - return createExpiredTokenError() - } - await ctx.db.transaction(async (dbTxn) => { + const accountService = ctx.services.account(dbTxn) const moderationTxn = ctx.services.moderation(dbTxn) const [currentAction] = await moderationTxn.getCurrentActions({ did }) if (currentAction?.action === TAKEDOWN) { // Do not disturb an existing takedown, continue with account deletion - return await removeDeleteToken(dbTxn, did) + return await accountService.deleteEmailToken(did, 'delete_account') } if (currentAction) { // Reverse existing action to replace it with a self-takedown @@ -74,7 +51,7 @@ export default function (server: Server, ctx: AppContext) { createdAt: now, }) await moderationTxn.takedownRepo({ did, takedownId: takedown.id }) - await removeDeleteToken(dbTxn, did) + await accountService.deleteEmailToken(did, 'delete_account') }) ctx.backgroundQueue.add(async (db) => { @@ -90,34 +67,3 @@ export default function (server: Server, ctx: AppContext) { }, }) } - -type ErrorResponse = { - status: number - error: string - message: string -} - -const minsToMs = 60 * 1000 - -const createInvalidTokenError = (): ErrorResponse & { - error: 'InvalidToken' -} => ({ - status: 400, - error: 'InvalidToken', - message: 'Token is invalid', -}) - -const createExpiredTokenError = (): ErrorResponse & { - error: 'ExpiredToken' -} => ({ - status: 400, - error: 'ExpiredToken', - message: 'The password reset token has expired', -}) - -const removeDeleteToken = async (db: Database, did: string) => { - await db.db - .deleteFrom('delete_account_token') - .where('delete_account_token.did', '=', did) - .execute() -} diff --git a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts index a448d97c02e..c438c32f69f 100644 --- a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts +++ b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts @@ -1,7 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { getRandomToken } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.requestAccountDelete({ @@ -12,15 +11,9 @@ export default function (server: Server, ctx: AppContext) { if (!user) { throw new InvalidRequestError('user not found') } - const token = getRandomToken().toUpperCase() - const requestedAt = new Date().toISOString() - await ctx.db.db - .insertInto('delete_account_token') - .values({ did, token, requestedAt }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ token, requestedAt }), - ) - .execute() + const token = await ctx.services + .account(ctx.db) + .createEmailToken(did, 'delete_account') await ctx.mailer.sendAccountDelete({ token }, { to: user.email }) }, }) diff --git a/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts b/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts index 5d81e43c68b..61b17ebb9a9 100644 --- a/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts +++ b/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts @@ -1,6 +1,5 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import { getRandomToken } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.requestPasswordReset(async ({ input }) => { @@ -9,16 +8,9 @@ export default function (server: Server, ctx: AppContext) { const user = await ctx.services.account(ctx.db).getAccountByEmail(email) if (user) { - const token = getRandomToken().toUpperCase() - const grantedAt = new Date().toISOString() - await ctx.db.db - .updateTable('user_account') - .where('did', '=', user.did) - .set({ - passwordResetToken: token, - passwordResetGrantedAt: grantedAt, - }) - .execute() + const token = await ctx.services + .account(ctx.db) + .createEmailToken(user.did, 'reset_password') await ctx.mailer.sendResetPassword( { handle: user.handle, token }, { to: user.email }, diff --git a/packages/pds/src/api/com/atproto/server/resetPassword.ts b/packages/pds/src/api/com/atproto/server/resetPassword.ts index de8d10382c0..a84b6249a3c 100644 --- a/packages/pds/src/api/com/atproto/server/resetPassword.ts +++ b/packages/pds/src/api/com/atproto/server/resetPassword.ts @@ -1,6 +1,5 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import Database from '../../../../db' import { MINUTE } from '@atproto/common' export default function (server: Server, ctx: AppContext) { @@ -14,69 +13,16 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ input }) => { const { token, password } = input.body - const tokenInfo = await ctx.db.db - .selectFrom('user_account') - .select(['did', 'passwordResetGrantedAt']) - .where('passwordResetToken', '=', token.toUpperCase()) - .executeTakeFirst() - - if (!tokenInfo?.passwordResetGrantedAt) { - return createInvalidTokenError() - } - - const now = new Date() - const grantedAt = new Date(tokenInfo.passwordResetGrantedAt) - const expiresAt = new Date(grantedAt.getTime() + 15 * minsToMs) - - if (now > expiresAt) { - await unsetResetToken(ctx.db, tokenInfo.did) - return createExpiredTokenError() - } + const did = await ctx.services + .account(ctx.db) + .assertValidTokenAndFindDid('reset_password', token) await ctx.db.transaction(async (dbTxn) => { - await unsetResetToken(dbTxn, tokenInfo.did) - await ctx.services - .account(dbTxn) - .updateUserPassword(tokenInfo.did, password) - await await ctx.services - .auth(dbTxn) - .revokeRefreshTokensByDid(tokenInfo.did) + const accountService = ctx.services.account(ctx.db) + await accountService.updateUserPassword(did, password) + await accountService.deleteEmailToken(did, 'reset_password') + await ctx.services.auth(dbTxn).revokeRefreshTokensByDid(did) }) }, }) } - -type ErrorResponse = { - status: number - error: string - message: string -} - -const minsToMs = 60 * 1000 - -const createInvalidTokenError = (): ErrorResponse & { - error: 'InvalidToken' -} => ({ - status: 400, - error: 'InvalidToken', - message: 'Token is invalid', -}) - -const createExpiredTokenError = (): ErrorResponse & { - error: 'ExpiredToken' -} => ({ - status: 400, - error: 'ExpiredToken', - message: 'The password reset token has expired', -}) - -const unsetResetToken = async (db: Database, did: string) => { - await db.db - .updateTable('user_account') - .where('did', '=', did) - .set({ - passwordResetToken: null, - passwordResetGrantedAt: null, - }) - .execute() -} diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index bebb13eb84d..b5ac5c6d265 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -14,7 +14,6 @@ import * as inviteCode from './tables/invite-code' import * as notification from './tables/user-notification' import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' -import * as deleteAccountToken from './tables/delete-account-token' import * as emailToken from './tables/email-token' import * as moderation from './tables/moderation' import * as mute from './tables/mute' @@ -43,7 +42,6 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType & notification.PartialDB & blob.PartialDB & repoBlob.PartialDB & - deleteAccountToken.PartialDB & emailToken.PartialDB & moderation.PartialDB & mute.PartialDB & diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts index d775f6f0a9c..c18b9002965 100644 --- a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts +++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts @@ -12,16 +12,49 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did']) .execute() + // for lookups where we don't know the did + await db.schema.createIndex('email_token_token_idx').on('token').execute() + await db.schema .alterTable('user_account') .addColumn('emailConfirmedAt', 'varchar') .execute() + + await db.schema + .alterTable('user_account') + .dropColumn('passwordResetToken') + .execute() + + await db.schema + .alterTable('user_account') + .dropColumn('passwordResetGrantedAt') + .execute() + + await db.schema.dropTable('delete_account_token').execute() } export async function down(db: Kysely): Promise { await db.schema.dropTable('email_token').execute() + await db.schema .alterTable('user_account') .dropColumn('emailConfirmedAt') .execute() + + await db.schema + .alterTable('user_account') + .addColumn('passwordResetToken', 'varchar') + .execute() + + await db.schema + .alterTable('user_account') + .addColumn('passwordResetGrantedAt', 'varchar') + .execute() + + await db.schema + .createTable('delete_account_token') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('token', 'varchar', (col) => col.notNull()) + .addColumn('requestedAt', 'varchar', (col) => col.notNull()) + .execute() } diff --git a/packages/pds/src/db/tables/delete-account-token.ts b/packages/pds/src/db/tables/delete-account-token.ts deleted file mode 100644 index da748c639a7..00000000000 --- a/packages/pds/src/db/tables/delete-account-token.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DeleteAccountToken { - did: string - token: string - requestedAt: string -} - -export const tableName = 'delete_account_token' - -export type PartialDB = { [tableName]: DeleteAccountToken } diff --git a/packages/pds/src/db/tables/user-account.ts b/packages/pds/src/db/tables/user-account.ts index ef9fdbecb3c..808663ca468 100644 --- a/packages/pds/src/db/tables/user-account.ts +++ b/packages/pds/src/db/tables/user-account.ts @@ -6,8 +6,6 @@ export interface UserAccount { passwordScrypt: string createdAt: string emailConfirmedAt: string | null - passwordResetToken: string | null - passwordResetGrantedAt: string | null invitesDisabled: Generated<0 | 1> inviteNote: string | null } diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index a2f85c63998..d8c1d02385c 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -589,7 +589,7 @@ export class AccountService { .selectAll() .where('purpose', '=', purpose) .where('did', '=', did) - .where('token', '=', token) + .where('token', '=', token.toUpperCase()) .executeTakeFirst() if (!res) { throw new InvalidRequestError('Token is invalid', 'InvalidToken') @@ -600,6 +600,27 @@ export class AccountService { } } + async assertValidTokenAndFindDid( + purpose: EmailTokenPurpose, + token: string, + expirationLen = 15 * MINUTE, + ): Promise { + const res = await this.db.db + .selectFrom('email_token') + .selectAll() + .where('purpose', '=', purpose) + .where('token', '=', token.toUpperCase()) + .executeTakeFirst() + if (!res) { + throw new InvalidRequestError('Token is invalid', 'InvalidToken') + } + const expired = !lessThanAgoMs(res.requestedAt, expirationLen) + if (expired) { + throw new InvalidRequestError('Token is expired', 'ExpiredToken') + } + return res.did + } + async getLastSeenNotifs(did: string): Promise { const res = await this.db.db .selectFrom('user_state') diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index 78a769b6e9f..75a679e384f 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -506,24 +506,23 @@ describe('account', () => { it('allows only unexpired password reset tokens', async () => { await agent.api.com.atproto.server.requestPasswordReset({ email }) - const user = await db.db - .updateTable('user_account') - .where('email', '=', email) + const res = await db.db + .updateTable('email_token') + .where('purpose', '=', 'reset_password') + .where('did', '=', did) .set({ - passwordResetGrantedAt: new Date( - Date.now() - 16 * minsToMs, - ).toISOString(), + requestedAt: new Date(Date.now() - 16 * minsToMs), }) - .returning(['passwordResetToken']) + .returning(['token']) .executeTakeFirst() - if (!user?.passwordResetToken) { + if (!res?.token) { throw new Error('Missing reset token') } // Use of expired token fails await expect( agent.api.com.atproto.server.resetPassword({ - token: user.passwordResetToken, + token: res.token, password: passwordAlt, }), ).rejects.toThrow(ComAtprotoServerResetPassword.ExpiredTokenError) From 8f903beb091a17c1aaa7259df27bc27cae9b50bd Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 27 Sep 2023 17:49:08 -0500 Subject: [PATCH 14/18] update constraint name --- .../src/db/migrations/20230926T195532354Z-email-tokens.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts index ce8e6574731..4200e64477b 100644 --- a/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts +++ b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts @@ -10,7 +10,10 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('token', 'varchar', (col) => col.notNull()) .addColumn('requestedAt', timestamp, (col) => col.notNull()) .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did']) - .addUniqueConstraint('email_token_token_unique', ['purpose', 'token']) + .addUniqueConstraint('email_token_purpose_token_unique', [ + 'purpose', + 'token', + ]) .execute() await db.schema From b9168c34276536aa20a4e209b1c04165b8453aa4 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 12:58:48 -0500 Subject: [PATCH 15/18] temporarily disable unconfirmed updates --- .../src/api/com/atproto/server/updateEmail.ts | 3 ++ packages/pds/tests/email-confirmation.test.ts | 30 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts index c87ffa16b82..477a849fcf0 100644 --- a/packages/pds/src/api/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts @@ -12,6 +12,9 @@ export default function (server: Server, ctx: AppContext) { if (!user) { throw new InvalidRequestError('user not found') } + if (!user.emailConfirmedAt) { + throw new InvalidRequestError('email must be confirmed (temporary)') + } // require valid token if (user.emailConfirmedAt) { if (!token) { diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts index 284e40d9123..bbcbdfa8e80 100644 --- a/packages/pds/tests/email-confirmation.test.ts +++ b/packages/pds/tests/email-confirmation.test.ts @@ -61,28 +61,50 @@ describe('email confirmation', () => { expect(session.data.emailConfirmed).toEqual(false) }) - it('allows email update without token when unverified', async () => { + it('disallows email update when unverified', async () => { const res = await agent.api.com.atproto.server.requestEmailUpdate( undefined, { headers: sc.getHeaders(alice.did) }, ) expect(res.data.tokenRequired).toBe(false) - await agent.api.com.atproto.server.updateEmail( + const attempt = agent.api.com.atproto.server.updateEmail( { email: 'new-alice@example.com', }, { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, ) + await expect(attempt).rejects.toThrow() const session = await agent.api.com.atproto.server.getSession( {}, { headers: sc.getHeaders(alice.did) }, ) - expect(session.data.email).toEqual('new-alice@example.com') + expect(session.data.email).toEqual(alice.email) expect(session.data.emailConfirmed).toEqual(false) - alice.email = session.data.email }) + // it('allows email update without token when unverified', async () => { + // const res = await agent.api.com.atproto.server.requestEmailUpdate( + // undefined, + // { headers: sc.getHeaders(alice.did) }, + // ) + // expect(res.data.tokenRequired).toBe(false) + + // await agent.api.com.atproto.server.updateEmail( + // { + // email: 'new-alice@example.com', + // }, + // { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + // ) + // const session = await agent.api.com.atproto.server.getSession( + // {}, + // { headers: sc.getHeaders(alice.did) }, + // ) + // expect(session.data.email).toEqual('new-alice@example.com') + // expect(session.data.emailConfirmed).toEqual(false) + // alice.email = session.data.email + // }) + let confirmToken it('requests email confirmation', async () => { From 5127187c54882736a908f57ee65f2fad192735e6 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 13:03:31 -0500 Subject: [PATCH 16/18] tidy --- lexicons/com/atproto/server/updateEmail.json | 2 +- packages/api/src/client/lexicons.ts | 2 +- packages/api/src/client/types/com/atproto/server/updateEmail.ts | 2 +- packages/bsky/src/lexicon/lexicons.ts | 2 +- .../bsky/src/lexicon/types/com/atproto/server/updateEmail.ts | 2 +- packages/pds/src/lexicon/lexicons.ts | 2 +- .../pds/src/lexicon/types/com/atproto/server/updateEmail.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json index c51aef5dd68..88872698910 100644 --- a/lexicons/com/atproto/server/updateEmail.json +++ b/lexicons/com/atproto/server/updateEmail.json @@ -14,7 +14,7 @@ "email": { "type": "string" }, "token": { "type": "string", - "description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed." + "description": "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed." } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 1a763baa6ba..77b4939d758 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3029,7 +3029,7 @@ export const schemaDict = { token: { type: 'string', description: - " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", }, }, }, diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts index b16864fe684..92aef734e20 100644 --- a/packages/api/src/client/types/com/atproto/server/updateEmail.ts +++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts @@ -11,7 +11,7 @@ export interface QueryParams {} export interface InputSchema { email: string - /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ token?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 1a763baa6ba..77b4939d758 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3029,7 +3029,7 @@ export const schemaDict = { token: { type: 'string', description: - " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", }, }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts index 17025f45a98..c88bd3021b2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -12,7 +12,7 @@ export interface QueryParams {} export interface InputSchema { email: string - /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ token?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 1a763baa6ba..77b4939d758 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3029,7 +3029,7 @@ export const schemaDict = { token: { type: 'string', description: - " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", }, }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts index 17025f45a98..c88bd3021b2 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -12,7 +12,7 @@ export interface QueryParams {} export interface InputSchema { email: string - /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ token?: string [k: string]: unknown } From e51ad36e8765d478c6533bd39caef63236dd3cce Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 13:10:59 -0500 Subject: [PATCH 17/18] fix some tests --- packages/bsky/tests/indexing.test.ts | 3 ++- packages/pds/tests/moderation.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index cee3ed5a768..f5c8083df09 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -648,8 +648,9 @@ describe('indexing', () => { headers: sc.getHeaders(alice), }) const { token } = await network.pds.ctx.db.db - .selectFrom('delete_account_token') + .selectFrom('email_token') .selectAll() + .where('purpose', '=', 'delete_account') .where('did', '=', alice) .executeTakeFirstOrThrow() await pdsAgent.api.com.atproto.server.deleteAccount({ diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index edbb23c6578..80536a6933f 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -286,7 +286,8 @@ describe('moderation', () => { headers: sc.getHeaders(deleteme.did), }) const { token: deletionToken } = await server.ctx.db.db - .selectFrom('delete_account_token') + .selectFrom('email_token') + .where('purpose', '=', 'delete_account') .where('did', '=', deleteme.did) .selectAll() .executeTakeFirstOrThrow() From e6ddbf5fada336fd53e895f30e094c3112a94b26 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 13:20:40 -0500 Subject: [PATCH 18/18] validate email syntax --- .../pds/src/api/com/atproto/server/updateEmail.ts | 6 ++++++ packages/pds/tests/email-confirmation.test.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts index 477a849fcf0..1873f5e0157 100644 --- a/packages/pds/src/api/com/atproto/server/updateEmail.ts +++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts @@ -1,6 +1,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' +import disposable from 'disposable-email' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.updateEmail({ @@ -8,6 +9,11 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, input }) => { const did = auth.credentials.did const { token, email } = input.body + if (!disposable.validate(email)) { + throw new InvalidRequestError( + 'This email address is not supported, please use a different email.', + ) + } const user = await ctx.services.account(ctx.db).getAccount(did) if (!user) { throw new InvalidRequestError('user not found') diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts index bbcbdfa8e80..fc3c4caadcd 100644 --- a/packages/pds/tests/email-confirmation.test.ts +++ b/packages/pds/tests/email-confirmation.test.ts @@ -204,6 +204,19 @@ describe('email confirmation', () => { ) }) + it('fails email update with a badly formatted email', async () => { + const attempt = agent.api.com.atproto.server.updateEmail( + { + email: 'bad-email@disposeamail.com', + token: updateToken, + }, + { headers: sc.getHeaders(alice.did), encoding: 'application/json' }, + ) + await expect(attempt).rejects.toThrow( + 'This email address is not supported, please use a different email.', + ) + }) + it('updates email', async () => { await agent.api.com.atproto.server.updateEmail( {