+14
.sqlx/query-92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439.json
+14
.sqlx/query-92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Uuid"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439"
14
+
}
+15
.sqlx/query-ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6.json
+15
.sqlx/query-ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Uuid"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6"
15
+
}
-28
.sqlx/query-d6ec64a262936b8def9e5cad7d021df408b3a9b80fce5a7446647fbf276092ca.json
-28
.sqlx/query-d6ec64a262936b8def9e5cad7d021df408b3a9b80fce5a7446647fbf276092ca.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT id, email FROM users WHERE did = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "id",
9
-
"type_info": "Uuid"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "email",
14
-
"type_info": "Text"
15
-
}
16
-
],
17
-
"parameters": {
18
-
"Left": [
19
-
"Text"
20
-
]
21
-
},
22
-
"nullable": [
23
-
false,
24
-
true
25
-
]
26
-
},
27
-
"hash": "d6ec64a262936b8def9e5cad7d021df408b3a9b80fce5a7446647fbf276092ca"
28
-
}
···
+40
.sqlx/query-e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840.json
+40
.sqlx/query-e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT id, handle, email, email_verified FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "handle",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "email",
19
+
"type_info": "Text"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "email_verified",
24
+
"type_info": "Bool"
25
+
}
26
+
],
27
+
"parameters": {
28
+
"Left": [
29
+
"Text"
30
+
]
31
+
},
32
+
"nullable": [
33
+
false,
34
+
false,
35
+
true,
36
+
false
37
+
]
38
+
},
39
+
"hash": "e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840"
40
+
}
-2
frontend/src/lib/api.ts
-2
frontend/src/lib/api.ts
+14
-1
frontend/src/locales/en.json
+14
-1
frontend/src/locales/en.json
···
234
"deleting": "Deleting...",
235
"messages": {
236
"emailCodeSent": "Verification code sent to your notification channel",
237
"emailUpdated": "Email updated successfully",
238
"emailUpdateFailed": "Failed to update email",
239
"handleUpdated": "Handle updated successfully",
···
659
"codeResent": "Verification code resent!",
660
"codeResentDetail": "Verification code sent! Check your inbox.",
661
"backToLogin": "Back to Login",
662
"verifyingAccount": "Verifying account: @{handle}",
663
"startOver": "Start over with a different account",
664
"noPending": "No pending verification found.",
···
671
"continue": "Continue",
672
"identifierLabel": "Email or Identifier",
673
"identifierPlaceholder": "you@example.com",
674
-
"identifierHelp": "The email address or identifier the code was sent to"
675
},
676
"resetPassword": {
677
"title": "Reset Password",
···
234
"deleting": "Deleting...",
235
"messages": {
236
"emailCodeSent": "Verification code sent to your notification channel",
237
+
"emailCodeSentToCurrent": "Verification code sent to your current email address",
238
"emailUpdated": "Email updated successfully",
239
"emailUpdateFailed": "Failed to update email",
240
"handleUpdated": "Handle updated successfully",
···
660
"codeResent": "Verification code resent!",
661
"codeResentDetail": "Verification code sent! Check your inbox.",
662
"backToLogin": "Back to Login",
663
+
"backToSettings": "Back to Settings",
664
"verifyingAccount": "Verifying account: @{handle}",
665
"startOver": "Start over with a different account",
666
"noPending": "No pending verification found.",
···
673
"continue": "Continue",
674
"identifierLabel": "Email or Identifier",
675
"identifierPlaceholder": "you@example.com",
676
+
"identifierHelp": "The email address or identifier the code was sent to",
677
+
"emailUpdateTitle": "Update Email Address",
678
+
"emailUpdateSubtitle": "Enter your new email address and the verification code sent to your current email.",
679
+
"emailUpdateRequiresAuth": "You must be signed in to update your email address.",
680
+
"emailUpdateFailed": "Failed to update email address",
681
+
"emailUpdateCodeHelp": "The code was sent to your current email address",
682
+
"newEmailLabel": "New Email Address",
683
+
"newEmailPlaceholder": "new@example.com",
684
+
"updateEmail": "Update Email",
685
+
"updating": "Updating...",
686
+
"emailUpdated": "Your email has been updated successfully.",
687
+
"emailUpdatedInfo": "You may need to verify your new email address."
688
},
689
"resetPassword": {
690
"title": "Reset Password",
+14
-17
frontend/src/locales/fi.json
+14
-17
frontend/src/locales/fi.json
···
234
"deleting": "Poistetaan...",
235
"messages": {
236
"emailCodeSent": "Vahvistuskoodi lähetetty ilmoituskanavallesi",
237
"emailUpdated": "Sähköposti päivitetty",
238
"emailUpdateFailed": "Sähköpostin päivitys epäonnistui",
239
"handleUpdated": "Käyttäjänimi päivitetty",
···
671
"noPending": "Odottavaa vahvistusta ei löytynyt.",
672
"noPendingInfo": "Jos loit tilin äskettäin ja sinun on vahvistettava se, sinun on ehkä luotava uusi tili. Jos olet jo vahvistanut tilisi, voit kirjautua sisään.",
673
"createAccount": "Luo tili",
674
-
"signIn": "Kirjaudu sisään"
675
},
676
"resetPassword": {
677
"title": "Palauta salasana",
···
754
"verificationMethod": "Vahvistusmenetelmä",
755
"email": "Sähköpostiosoite",
756
"emailPlaceholder": "sinä@esimerkki.fi",
757
-
"discord": "Discord",
758
-
"discordId": "Discord-käyttäjätunnus",
759
-
"discordIdPlaceholder": "Discord-käyttäjätunnuksesi",
760
-
"discordIdHint": "Numeerinen Discord-käyttäjätunnuksesi (ota Kehittäjätila käyttöön löytääksesi sen)",
761
-
"telegram": "Telegram",
762
-
"telegramUsername": "Telegram-käyttäjänimi",
763
-
"telegramUsernamePlaceholder": "@käyttäjänimesi",
764
-
"signal": "Signal",
765
-
"signalNumber": "Signal-puhelinnumero",
766
-
"signalNumberPlaceholder": "+358401234567",
767
-
"signalNumberHint": "Sisällytä maakoodi (esim. +358 Suomelle)",
768
"inviteCode": "Kutsukoodi",
769
"inviteCodePlaceholder": "Syötä kutsukoodisi",
770
-
"inviteCodeRequired": "vaaditaan",
771
-
"didWebDescription": "Käytä DID-identiteettiä, jota isännöidään omalla verkkotunnuksellasi.",
772
-
"didWebToggle": "Käytä ulkoista did:web",
773
"externalDid": "Sinun did:web",
774
"externalDidPlaceholder": "did:web:verkkotunnuksesi.fi",
775
-
"dnsVerificationInstructions": "Vahvistaaksesi verkkotunnuksesi, lisää tämä TXT-tietue:",
776
-
"copyDid": "Kopioi DID",
777
"createButton": "Luo tili",
778
"creating": "Luodaan...",
779
"alreadyHaveAccount": "Onko sinulla jo tili?",
···
234
"deleting": "Poistetaan...",
235
"messages": {
236
"emailCodeSent": "Vahvistuskoodi lähetetty ilmoituskanavallesi",
237
+
"emailCodeSentToCurrent": "Vahvistuskoodi lähetetty nykyiseen sähköpostiosoitteeseesi",
238
"emailUpdated": "Sähköposti päivitetty",
239
"emailUpdateFailed": "Sähköpostin päivitys epäonnistui",
240
"handleUpdated": "Käyttäjänimi päivitetty",
···
672
"noPending": "Odottavaa vahvistusta ei löytynyt.",
673
"noPendingInfo": "Jos loit tilin äskettäin ja sinun on vahvistettava se, sinun on ehkä luotava uusi tili. Jos olet jo vahvistanut tilisi, voit kirjautua sisään.",
674
"createAccount": "Luo tili",
675
+
"signIn": "Kirjaudu sisään",
676
+
"backToSettings": "Takaisin asetuksiin",
677
+
"emailUpdateCodeHelp": "Koodi lähetettiin nykyiseen sähköpostiosoitteeseesi",
678
+
"emailUpdateFailed": "Sähköpostiosoitteen päivitys epäonnistui",
679
+
"emailUpdateRequiresAuth": "Sinun on kirjauduttava sisään päivittääksesi sähköpostiosoitteesi.",
680
+
"emailUpdateSubtitle": "Syötä uusi sähköpostiosoitteesi ja nykyiseen sähköpostiisi lähetetty vahvistuskoodi.",
681
+
"emailUpdateTitle": "Päivitä sähköpostiosoite",
682
+
"emailUpdated": "Sähköpostiosoitteesi on päivitetty.",
683
+
"emailUpdatedInfo": "Sinun on ehkä vahvistettava uusi sähköpostiosoitteesi.",
684
+
"newEmailLabel": "Uusi sähköpostiosoite",
685
+
"newEmailPlaceholder": "uusi@esimerkki.fi",
686
+
"updateEmail": "Päivitä sähköposti",
687
+
"updating": "Päivitetään..."
688
},
689
"resetPassword": {
690
"title": "Palauta salasana",
···
767
"verificationMethod": "Vahvistusmenetelmä",
768
"email": "Sähköpostiosoite",
769
"emailPlaceholder": "sinä@esimerkki.fi",
770
"inviteCode": "Kutsukoodi",
771
"inviteCodePlaceholder": "Syötä kutsukoodisi",
772
"externalDid": "Sinun did:web",
773
"externalDidPlaceholder": "did:web:verkkotunnuksesi.fi",
774
"createButton": "Luo tili",
775
"creating": "Luodaan...",
776
"alreadyHaveAccount": "Onko sinulla jo tili?",
+16
-41
frontend/src/locales/ja.json
+16
-41
frontend/src/locales/ja.json
···
234
"deleting": "削除中...",
235
"messages": {
236
"emailCodeSent": "通知チャンネルに確認コードを送信しました",
237
"emailUpdated": "メールを更新しました",
238
"emailUpdateFailed": "メールの更新に失敗しました",
239
"handleUpdated": "ハンドルを更新しました",
···
671
"noPending": "保留中の確認が見つかりません。",
672
"noPendingInfo": "最近アカウントを作成して確認が必要な場合は、新しいアカウントを作成する必要があります。すでにアカウントを確認した場合は、サインインできます。",
673
"createAccount": "アカウントを作成",
674
-
"signIn": "サインイン"
675
},
676
"resetPassword": {
677
"title": "パスワードリセット",
···
754
"verificationMethod": "確認方法",
755
"email": "メールアドレス",
756
"emailPlaceholder": "you@example.com",
757
-
"discord": "Discord",
758
-
"discordId": "Discord ユーザー ID",
759
-
"discordIdPlaceholder": "Discord ユーザー ID",
760
-
"discordIdHint": "数値の Discord ユーザー ID(開発者モードを有効にして確認)",
761
-
"telegram": "Telegram",
762
-
"telegramUsername": "Telegram ユーザー名",
763
-
"telegramUsernamePlaceholder": "@yourusername",
764
-
"signal": "Signal",
765
-
"signalNumber": "Signal 電話番号",
766
-
"signalNumberPlaceholder": "+81XXXXXXXXXX",
767
-
"signalNumberHint": "国番号を含めてください(例: 日本は +81)",
768
"inviteCode": "招待コード",
769
"inviteCodePlaceholder": "招待コードを入力",
770
-
"inviteCodeRequired": "必須",
771
-
"didWebDescription": "独自ドメインでホストされる DID アイデンティティを使用します。",
772
-
"didWebToggle": "外部 did:web を使用",
773
"externalDid": "あなたの did:web",
774
"externalDidPlaceholder": "did:web:yourdomain.com",
775
-
"dnsVerificationInstructions": "ドメインを確認するには、この TXT レコードを追加してください:",
776
-
"copyDid": "DID をコピー",
777
"createButton": "アカウントを作成",
778
"creating": "作成中...",
779
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
···
896
"delegation": {
897
"title": "アカウント委任",
898
"controllers": "コントローラー",
899
-
"controllersDescription": "コントローラーはあなたのアカウントの管理者として行動できます。あなたが許可した操作を実行し、あなたの代わりに投稿を作成し、リポジトリを変更できます。",
900
"controlledAccounts": "管理アカウント",
901
-
"controlledAccountsDescription": "これらはあなたがコントローラーとして追加されているアカウントです。これらのアカウントで許可されたアクションを実行できます。",
902
"noControllers": "コントローラーはまだいません",
903
"noControlledAccounts": "管理アカウントはありません",
904
"addController": "コントローラーを追加",
905
-
"revokeAccess": "アクセスを取り消す",
906
-
"revokeConfirm": "このコントローラーのアクセスを取り消しますか?あなたのアカウントで操作できなくなります。",
907
"handle": "ハンドル",
908
-
"handlePlaceholder": "@user.bsky.social",
909
"did": "DID",
910
-
"didPlaceholder": "did:plc:...",
911
-
"scopes": "権限レベル",
912
"scopeOwner": "オーナー",
913
-
"scopeOwnerDesc": "完全な管理(すべてのアクションを実行可能)",
914
-
"scopeAdmin": "管理者",
915
-
"scopeAdminDesc": "投稿、アプリパスワード、設定の管理",
916
-
"scopeEditor": "編集者",
917
-
"scopeEditorDesc": "投稿、いいね、フォローの作成・管理",
918
"scopeViewer": "閲覧者",
919
-
"scopeViewerDesc": "リポジトリと設定の読み取り専用アクセス",
920
"scopeCustom": "カスタム",
921
-
"scopeCustomDesc": "個別の権限を選択",
922
-
"grantedAt": "許可日時",
923
-
"expiresAt": "有効期限",
924
-
"noExpiration": "無期限",
925
"actAs": "として行動",
926
"auditLog": "監査ログ",
927
"auditLogTitle": "委任監査ログ",
···
944
"showing": "{start}~{end} / {total}件",
945
"refresh": "更新",
946
"failedToLoadAuditLog": "監査ログの読み込みに失敗しました",
947
-
"addControllerTitle": "コントローラーを追加",
948
-
"addControllerDescription": "このアカウントに対して指定した権限で操作できるユーザーを追加します。",
949
-
"controllerIdentifier": "コントローラーのハンドルまたはDID",
950
-
"selectScopes": "権限レベルを選択",
951
-
"add": "追加",
952
"adding": "追加中...",
953
-
"cancel": "キャンセル",
954
"accessLevel": "アクセスレベル",
955
"addControllerButton": "+ コントローラーを追加",
956
"auditLogDesc": "すべての委任アクティビティを表示",
···
974
"remove": "削除",
975
"removeConfirm": "このコントローラーを削除しますか?",
976
"viewAuditLog": "監査ログを表示",
977
-
"yourAccessLevel": "あなたのアクセスレベル"
978
},
979
"actAs": {
980
"title": "として行動",
···
234
"deleting": "削除中...",
235
"messages": {
236
"emailCodeSent": "通知チャンネルに確認コードを送信しました",
237
+
"emailCodeSentToCurrent": "現在のメールアドレスに確認コードを送信しました",
238
"emailUpdated": "メールを更新しました",
239
"emailUpdateFailed": "メールの更新に失敗しました",
240
"handleUpdated": "ハンドルを更新しました",
···
672
"noPending": "保留中の確認が見つかりません。",
673
"noPendingInfo": "最近アカウントを作成して確認が必要な場合は、新しいアカウントを作成する必要があります。すでにアカウントを確認した場合は、サインインできます。",
674
"createAccount": "アカウントを作成",
675
+
"signIn": "サインイン",
676
+
"backToSettings": "設定に戻る",
677
+
"emailUpdateCodeHelp": "コードは現在のメールアドレスに送信されました",
678
+
"emailUpdateFailed": "メールアドレスの更新に失敗しました",
679
+
"emailUpdateRequiresAuth": "メールアドレスを更新するにはサインインが必要です。",
680
+
"emailUpdateSubtitle": "新しいメールアドレスと、現在のメールに送信された確認コードを入力してください。",
681
+
"emailUpdateTitle": "メールアドレスの更新",
682
+
"emailUpdated": "メールアドレスが正常に更新されました。",
683
+
"emailUpdatedInfo": "新しいメールアドレスの確認が必要な場合があります。",
684
+
"newEmailLabel": "新しいメールアドレス",
685
+
"newEmailPlaceholder": "new@example.com",
686
+
"updateEmail": "メールを更新",
687
+
"updating": "更新中..."
688
},
689
"resetPassword": {
690
"title": "パスワードリセット",
···
767
"verificationMethod": "確認方法",
768
"email": "メールアドレス",
769
"emailPlaceholder": "you@example.com",
770
"inviteCode": "招待コード",
771
"inviteCodePlaceholder": "招待コードを入力",
772
"externalDid": "あなたの did:web",
773
"externalDidPlaceholder": "did:web:yourdomain.com",
774
"createButton": "アカウントを作成",
775
"creating": "作成中...",
776
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
···
893
"delegation": {
894
"title": "アカウント委任",
895
"controllers": "コントローラー",
896
"controlledAccounts": "管理アカウント",
897
"noControllers": "コントローラーはまだいません",
898
"noControlledAccounts": "管理アカウントはありません",
899
"addController": "コントローラーを追加",
900
"handle": "ハンドル",
901
"did": "DID",
902
"scopeOwner": "オーナー",
903
"scopeViewer": "閲覧者",
904
"scopeCustom": "カスタム",
905
"actAs": "として行動",
906
"auditLog": "監査ログ",
907
"auditLogTitle": "委任監査ログ",
···
924
"showing": "{start}~{end} / {total}件",
925
"refresh": "更新",
926
"failedToLoadAuditLog": "監査ログの読み込みに失敗しました",
927
"adding": "追加中...",
928
"accessLevel": "アクセスレベル",
929
"addControllerButton": "+ コントローラーを追加",
930
"auditLogDesc": "すべての委任アクティビティを表示",
···
948
"remove": "削除",
949
"removeConfirm": "このコントローラーを削除しますか?",
950
"viewAuditLog": "監査ログを表示",
951
+
"yourAccessLevel": "あなたのアクセスレベル",
952
+
"accountCreated": "委任アカウントを作成しました: {handle}"
953
},
954
"actAs": {
955
"title": "として行動",
+16
-41
frontend/src/locales/ko.json
+16
-41
frontend/src/locales/ko.json
···
234
"deleting": "삭제 중...",
235
"messages": {
236
"emailCodeSent": "알림 채널로 인증 코드를 보냈습니다",
237
"emailUpdated": "이메일이 업데이트되었습니다",
238
"emailUpdateFailed": "이메일 업데이트에 실패했습니다",
239
"handleUpdated": "핸들이 업데이트되었습니다",
···
671
"noPending": "보류 중인 인증이 없습니다.",
672
"noPendingInfo": "최근에 계정을 만들고 인증이 필요한 경우 새 계정을 만들어야 합니다. 이미 계정을 인증한 경우 로그인할 수 있습니다.",
673
"createAccount": "계정 만들기",
674
-
"signIn": "로그인"
675
},
676
"resetPassword": {
677
"title": "비밀번호 재설정",
···
754
"verificationMethod": "인증 방법",
755
"email": "이메일 주소",
756
"emailPlaceholder": "you@example.com",
757
-
"discord": "Discord",
758
-
"discordId": "Discord 사용자 ID",
759
-
"discordIdPlaceholder": "Discord 사용자 ID",
760
-
"discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
761
-
"telegram": "Telegram",
762
-
"telegramUsername": "Telegram 사용자 이름",
763
-
"telegramUsernamePlaceholder": "@yourusername",
764
-
"signal": "Signal",
765
-
"signalNumber": "Signal 전화번호",
766
-
"signalNumberPlaceholder": "+821012345678",
767
-
"signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
768
"inviteCode": "초대 코드",
769
"inviteCodePlaceholder": "초대 코드 입력",
770
-
"inviteCodeRequired": "필수",
771
-
"didWebDescription": "자체 도메인에서 호스팅되는 DID 아이덴티티를 사용합니다.",
772
-
"didWebToggle": "외부 did:web 사용",
773
"externalDid": "귀하의 did:web",
774
"externalDidPlaceholder": "did:web:yourdomain.com",
775
-
"dnsVerificationInstructions": "도메인을 인증하려면 이 TXT 레코드를 추가하세요:",
776
-
"copyDid": "DID 복사",
777
"createButton": "계정 만들기",
778
"creating": "생성 중...",
779
"alreadyHaveAccount": "이미 계정이 있으신가요?",
···
896
"delegation": {
897
"title": "계정 위임",
898
"controllers": "컨트롤러",
899
-
"controllersDescription": "컨트롤러는 귀하의 계정 관리자로서 행동할 수 있습니다. 귀하가 허용한 작업을 수행하고, 귀하를 대신하여 게시물을 생성하고, 저장소를 수정할 수 있습니다.",
900
"controlledAccounts": "관리 계정",
901
-
"controlledAccountsDescription": "귀하가 컨트롤러로 추가된 계정들입니다. 이 계정들에서 허용된 작업을 수행할 수 있습니다.",
902
"noControllers": "아직 컨트롤러가 없습니다",
903
"noControlledAccounts": "관리 계정이 없습니다",
904
"addController": "컨트롤러 추가",
905
-
"revokeAccess": "액세스 취소",
906
-
"revokeConfirm": "이 컨트롤러의 액세스를 취소하시겠습니까? 귀하의 계정에서 더 이상 작업을 수행할 수 없습니다.",
907
"handle": "핸들",
908
-
"handlePlaceholder": "@user.bsky.social",
909
"did": "DID",
910
-
"didPlaceholder": "did:plc:...",
911
-
"scopes": "권한 수준",
912
"scopeOwner": "소유자",
913
-
"scopeOwnerDesc": "전체 관리(모든 작업 수행 가능)",
914
-
"scopeAdmin": "관리자",
915
-
"scopeAdminDesc": "게시물, 앱 비밀번호, 설정 관리",
916
-
"scopeEditor": "편집자",
917
-
"scopeEditorDesc": "게시물, 좋아요, 팔로우 생성 및 관리",
918
"scopeViewer": "뷰어",
919
-
"scopeViewerDesc": "저장소 및 설정 읽기 전용 액세스",
920
"scopeCustom": "사용자 정의",
921
-
"scopeCustomDesc": "개별 권한 선택",
922
-
"grantedAt": "허용 일시",
923
-
"expiresAt": "만료",
924
-
"noExpiration": "무기한",
925
"actAs": "로 활동",
926
"auditLog": "감사 로그",
927
"auditLogTitle": "위임 감사 로그",
···
944
"showing": "{start}~{end} / {total}개",
945
"refresh": "새로고침",
946
"failedToLoadAuditLog": "감사 로그를 불러오지 못했습니다",
947
-
"addControllerTitle": "컨트롤러 추가",
948
-
"addControllerDescription": "이 계정에서 지정된 권한으로 작업할 수 있는 사용자를 추가합니다.",
949
-
"controllerIdentifier": "컨트롤러 핸들 또는 DID",
950
-
"selectScopes": "권한 수준 선택",
951
-
"add": "추가",
952
"adding": "추가 중...",
953
-
"cancel": "취소",
954
"accessLevel": "액세스 수준",
955
"addControllerButton": "+ 컨트롤러 추가",
956
"auditLogDesc": "모든 위임 활동 보기",
···
974
"remove": "제거",
975
"removeConfirm": "이 컨트롤러를 제거하시겠습니까?",
976
"viewAuditLog": "감사 로그 보기",
977
-
"yourAccessLevel": "귀하의 액세스 수준"
978
},
979
"actAs": {
980
"title": "로 활동",
···
234
"deleting": "삭제 중...",
235
"messages": {
236
"emailCodeSent": "알림 채널로 인증 코드를 보냈습니다",
237
+
"emailCodeSentToCurrent": "현재 이메일 주소로 인증 코드를 보냈습니다",
238
"emailUpdated": "이메일이 업데이트되었습니다",
239
"emailUpdateFailed": "이메일 업데이트에 실패했습니다",
240
"handleUpdated": "핸들이 업데이트되었습니다",
···
672
"noPending": "보류 중인 인증이 없습니다.",
673
"noPendingInfo": "최근에 계정을 만들고 인증이 필요한 경우 새 계정을 만들어야 합니다. 이미 계정을 인증한 경우 로그인할 수 있습니다.",
674
"createAccount": "계정 만들기",
675
+
"signIn": "로그인",
676
+
"backToSettings": "설정으로 돌아가기",
677
+
"emailUpdateCodeHelp": "코드가 현재 이메일 주소로 전송되었습니다",
678
+
"emailUpdateFailed": "이메일 주소 업데이트 실패",
679
+
"emailUpdateRequiresAuth": "이메일 주소를 업데이트하려면 로그인해야 합니다.",
680
+
"emailUpdateSubtitle": "새 이메일 주소와 현재 이메일로 전송된 인증 코드를 입력하세요.",
681
+
"emailUpdateTitle": "이메일 주소 업데이트",
682
+
"emailUpdated": "이메일 주소가 성공적으로 업데이트되었습니다.",
683
+
"emailUpdatedInfo": "새 이메일 주소를 인증해야 할 수 있습니다.",
684
+
"newEmailLabel": "새 이메일 주소",
685
+
"newEmailPlaceholder": "new@example.com",
686
+
"updateEmail": "이메일 업데이트",
687
+
"updating": "업데이트 중..."
688
},
689
"resetPassword": {
690
"title": "비밀번호 재설정",
···
767
"verificationMethod": "인증 방법",
768
"email": "이메일 주소",
769
"emailPlaceholder": "you@example.com",
770
"inviteCode": "초대 코드",
771
"inviteCodePlaceholder": "초대 코드 입력",
772
"externalDid": "귀하의 did:web",
773
"externalDidPlaceholder": "did:web:yourdomain.com",
774
"createButton": "계정 만들기",
775
"creating": "생성 중...",
776
"alreadyHaveAccount": "이미 계정이 있으신가요?",
···
893
"delegation": {
894
"title": "계정 위임",
895
"controllers": "컨트롤러",
896
"controlledAccounts": "관리 계정",
897
"noControllers": "아직 컨트롤러가 없습니다",
898
"noControlledAccounts": "관리 계정이 없습니다",
899
"addController": "컨트롤러 추가",
900
"handle": "핸들",
901
"did": "DID",
902
"scopeOwner": "소유자",
903
"scopeViewer": "뷰어",
904
"scopeCustom": "사용자 정의",
905
"actAs": "로 활동",
906
"auditLog": "감사 로그",
907
"auditLogTitle": "위임 감사 로그",
···
924
"showing": "{start}~{end} / {total}개",
925
"refresh": "새로고침",
926
"failedToLoadAuditLog": "감사 로그를 불러오지 못했습니다",
927
"adding": "추가 중...",
928
"accessLevel": "액세스 수준",
929
"addControllerButton": "+ 컨트롤러 추가",
930
"auditLogDesc": "모든 위임 활동 보기",
···
948
"remove": "제거",
949
"removeConfirm": "이 컨트롤러를 제거하시겠습니까?",
950
"viewAuditLog": "감사 로그 보기",
951
+
"yourAccessLevel": "귀하의 액세스 수준",
952
+
"accountCreated": "위임 계정이 생성되었습니다: {handle}"
953
},
954
"actAs": {
955
"title": "로 활동",
+16
-41
frontend/src/locales/sv.json
+16
-41
frontend/src/locales/sv.json
···
234
"deleting": "Raderar...",
235
"messages": {
236
"emailCodeSent": "Verifieringskod skickad till din meddelandekanal",
237
"emailUpdated": "E-post uppdaterad",
238
"emailUpdateFailed": "Kunde inte uppdatera e-post",
239
"handleUpdated": "Användarnamn uppdaterat",
···
671
"noPending": "Ingen väntande verifiering hittades.",
672
"noPendingInfo": "Om du nyligen skapade ett konto och behöver verifiera det kan du behöva skapa ett nytt konto. Om du redan verifierat ditt konto kan du logga in.",
673
"createAccount": "Skapa konto",
674
-
"signIn": "Logga in"
675
},
676
"resetPassword": {
677
"title": "Återställ lösenord",
···
754
"verificationMethod": "Verifieringsmetod",
755
"email": "E-postadress",
756
"emailPlaceholder": "du@exempel.se",
757
-
"discord": "Discord",
758
-
"discordId": "Discord användar-ID",
759
-
"discordIdPlaceholder": "Ditt Discord användar-ID",
760
-
"discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
761
-
"telegram": "Telegram",
762
-
"telegramUsername": "Telegram-användarnamn",
763
-
"telegramUsernamePlaceholder": "@dittanvändarnamn",
764
-
"signal": "Signal",
765
-
"signalNumber": "Signal-telefonnummer",
766
-
"signalNumberPlaceholder": "+46701234567",
767
-
"signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
768
"inviteCode": "Inbjudningskod",
769
"inviteCodePlaceholder": "Ange din inbjudningskod",
770
-
"inviteCodeRequired": "krävs",
771
-
"didWebDescription": "Använd en DID-identitet som är lagrad på din egen domän.",
772
-
"didWebToggle": "Använd extern did:web",
773
"externalDid": "Din did:web",
774
"externalDidPlaceholder": "did:web:dindomän.se",
775
-
"dnsVerificationInstructions": "För att verifiera din domän, lägg till denna TXT-post:",
776
-
"copyDid": "Kopiera DID",
777
"createButton": "Skapa konto",
778
"creating": "Skapar...",
779
"alreadyHaveAccount": "Har du redan ett konto?",
···
896
"delegation": {
897
"title": "Kontodelegering",
898
"controllers": "Kontrollanter",
899
-
"controllersDescription": "Kontrollanter kan agera som administratörer för ditt konto. De kan utföra åtgärder du tillåter, skapa inlägg för din räkning och modifiera din dataförvaring.",
900
"controlledAccounts": "Kontrollerade konton",
901
-
"controlledAccountsDescription": "Detta är konton där du har lagts till som kontrollant. Du kan utföra tillåtna åtgärder på dessa konton.",
902
"noControllers": "Inga kontrollanter ännu",
903
"noControlledAccounts": "Inga kontrollerade konton",
904
"addController": "Lägg till kontrollant",
905
-
"revokeAccess": "Återkalla åtkomst",
906
-
"revokeConfirm": "Återkalla denna kontrollants åtkomst? De kommer inte längre kunna utföra åtgärder på ditt konto.",
907
"handle": "Användarnamn",
908
-
"handlePlaceholder": "@user.bsky.social",
909
"did": "DID",
910
-
"didPlaceholder": "did:plc:...",
911
-
"scopes": "Behörighetsnivå",
912
"scopeOwner": "Ägare",
913
-
"scopeOwnerDesc": "Fullständig kontroll (kan utföra alla åtgärder)",
914
-
"scopeAdmin": "Administratör",
915
-
"scopeAdminDesc": "Hantera inlägg, applösenord, inställningar",
916
-
"scopeEditor": "Redaktör",
917
-
"scopeEditorDesc": "Skapa och hantera inlägg, gillningar, följningar",
918
"scopeViewer": "Läsare",
919
-
"scopeViewerDesc": "Endast läsåtkomst till dataförvaring och inställningar",
920
"scopeCustom": "Anpassad",
921
-
"scopeCustomDesc": "Välj individuella behörigheter",
922
-
"grantedAt": "Beviljad",
923
-
"expiresAt": "Upphör",
924
-
"noExpiration": "Ingen utgång",
925
"actAs": "Agera som",
926
"auditLog": "Granskningslogg",
927
"auditLogTitle": "Delegerings-granskningslogg",
···
944
"showing": "{start}–{end} av {total}",
945
"refresh": "Uppdatera",
946
"failedToLoadAuditLog": "Kunde inte ladda granskningsloggen",
947
-
"addControllerTitle": "Lägg till kontrollant",
948
-
"addControllerDescription": "Lägg till en användare som kan utföra åtgärder på detta konto med specificerade behörigheter.",
949
-
"controllerIdentifier": "Kontrollantens användarnamn eller DID",
950
-
"selectScopes": "Välj behörighetsnivå",
951
-
"add": "Lägg till",
952
"adding": "Lägger till...",
953
-
"cancel": "Avbryt",
954
"accessLevel": "Åtkomstnivå",
955
"addControllerButton": "+ Lägg till kontrollant",
956
"auditLogDesc": "Visa all delegeringsaktivitet",
···
974
"remove": "Ta bort",
975
"removeConfirm": "Vill du ta bort denna kontrollant?",
976
"viewAuditLog": "Visa granskningslogg",
977
-
"yourAccessLevel": "Din åtkomstnivå"
978
},
979
"actAs": {
980
"title": "Agera som",
···
234
"deleting": "Raderar...",
235
"messages": {
236
"emailCodeSent": "Verifieringskod skickad till din meddelandekanal",
237
+
"emailCodeSentToCurrent": "Verifieringskod skickad till din nuvarande e-postadress",
238
"emailUpdated": "E-post uppdaterad",
239
"emailUpdateFailed": "Kunde inte uppdatera e-post",
240
"handleUpdated": "Användarnamn uppdaterat",
···
672
"noPending": "Ingen väntande verifiering hittades.",
673
"noPendingInfo": "Om du nyligen skapade ett konto och behöver verifiera det kan du behöva skapa ett nytt konto. Om du redan verifierat ditt konto kan du logga in.",
674
"createAccount": "Skapa konto",
675
+
"signIn": "Logga in",
676
+
"backToSettings": "Tillbaka till inställningar",
677
+
"emailUpdateCodeHelp": "Koden skickades till din nuvarande e-postadress",
678
+
"emailUpdateFailed": "Kunde inte uppdatera e-postadress",
679
+
"emailUpdateRequiresAuth": "Du måste vara inloggad för att uppdatera din e-postadress.",
680
+
"emailUpdateSubtitle": "Ange din nya e-postadress och verifieringskoden som skickades till din nuvarande e-post.",
681
+
"emailUpdateTitle": "Uppdatera e-postadress",
682
+
"emailUpdated": "Din e-postadress har uppdaterats.",
683
+
"emailUpdatedInfo": "Du kan behöva verifiera din nya e-postadress.",
684
+
"newEmailLabel": "Ny e-postadress",
685
+
"newEmailPlaceholder": "ny@exempel.se",
686
+
"updateEmail": "Uppdatera e-post",
687
+
"updating": "Uppdaterar..."
688
},
689
"resetPassword": {
690
"title": "Återställ lösenord",
···
767
"verificationMethod": "Verifieringsmetod",
768
"email": "E-postadress",
769
"emailPlaceholder": "du@exempel.se",
770
"inviteCode": "Inbjudningskod",
771
"inviteCodePlaceholder": "Ange din inbjudningskod",
772
"externalDid": "Din did:web",
773
"externalDidPlaceholder": "did:web:dindomän.se",
774
"createButton": "Skapa konto",
775
"creating": "Skapar...",
776
"alreadyHaveAccount": "Har du redan ett konto?",
···
893
"delegation": {
894
"title": "Kontodelegering",
895
"controllers": "Kontrollanter",
896
"controlledAccounts": "Kontrollerade konton",
897
"noControllers": "Inga kontrollanter ännu",
898
"noControlledAccounts": "Inga kontrollerade konton",
899
"addController": "Lägg till kontrollant",
900
"handle": "Användarnamn",
901
"did": "DID",
902
"scopeOwner": "Ägare",
903
"scopeViewer": "Läsare",
904
"scopeCustom": "Anpassad",
905
"actAs": "Agera som",
906
"auditLog": "Granskningslogg",
907
"auditLogTitle": "Delegerings-granskningslogg",
···
924
"showing": "{start}–{end} av {total}",
925
"refresh": "Uppdatera",
926
"failedToLoadAuditLog": "Kunde inte ladda granskningsloggen",
927
"adding": "Lägger till...",
928
"accessLevel": "Åtkomstnivå",
929
"addControllerButton": "+ Lägg till kontrollant",
930
"auditLogDesc": "Visa all delegeringsaktivitet",
···
948
"remove": "Ta bort",
949
"removeConfirm": "Vill du ta bort denna kontrollant?",
950
"viewAuditLog": "Visa granskningslogg",
951
+
"yourAccessLevel": "Din åtkomstnivå",
952
+
"accountCreated": "Skapade delegerat konto: {handle}"
953
},
954
"actAs": {
955
"title": "Agera som",
+16
-25
frontend/src/locales/zh.json
+16
-25
frontend/src/locales/zh.json
···
234
"deleting": "删除中...",
235
"messages": {
236
"emailCodeSent": "验证码已发送到您的通知渠道",
237
"emailUpdated": "邮箱更新成功",
238
"emailUpdateFailed": "邮箱更新失败",
239
"handleUpdated": "用户名更新成功",
···
671
"continue": "继续",
672
"identifierLabel": "邮箱或标识符",
673
"identifierPlaceholder": "you@example.com",
674
-
"identifierHelp": "接收验证码的邮箱地址或标识符"
675
},
676
"resetPassword": {
677
"title": "重置密码",
···
821
"passkeysNotSupported": "此浏览器不支持通行密钥。请使用其他浏览器或使用密码注册。",
822
"passkeyCancelled": "通行密钥创建已取消",
823
"passkeyFailed": "通行密钥注册失败"
824
-
}
825
},
826
"trustedDevices": {
827
"title": "受信任设备",
···
879
"delegation": {
880
"title": "账户委托",
881
"controllers": "控制者",
882
-
"controllersDescription": "控制者可以作为您账户的管理员。他们可以执行您允许的操作,代表您发布帖子,以及修改您的数据仓库。",
883
"controlledAccounts": "受控账户",
884
-
"controlledAccountsDescription": "这些是您被添加为控制者的账户。您可以在这些账户上执行允许的操作。",
885
"noControllers": "暂无控制者",
886
"noControlledAccounts": "无受控账户",
887
"addController": "添加控制者",
888
-
"revokeAccess": "撤销访问",
889
-
"revokeConfirm": "撤销此控制者的访问权限?他们将无法再在您的账户上执行操作。",
890
"handle": "用户名",
891
-
"handlePlaceholder": "@user.bsky.social",
892
"did": "DID",
893
-
"didPlaceholder": "did:plc:...",
894
-
"scopes": "权限级别",
895
"scopeOwner": "所有者",
896
-
"scopeOwnerDesc": "完全控制(可执行所有操作)",
897
-
"scopeAdmin": "管理员",
898
-
"scopeAdminDesc": "管理帖子、应用专用密码、设置",
899
-
"scopeEditor": "编辑者",
900
-
"scopeEditorDesc": "创建和管理帖子、点赞、关注",
901
"scopeViewer": "查看者",
902
-
"scopeViewerDesc": "只读访问数据仓库和设置",
903
"scopeCustom": "自定义",
904
-
"scopeCustomDesc": "选择单独的权限",
905
-
"grantedAt": "授权时间",
906
-
"expiresAt": "过期时间",
907
-
"noExpiration": "永不过期",
908
"actAs": "代理操作",
909
"auditLog": "审计日志",
910
"auditLogTitle": "委托审计日志",
···
928
"showing": "{start}–{end} / 共{total}条",
929
"refresh": "刷新",
930
"failedToLoadAuditLog": "加载审计日志失败",
931
-
"addControllerTitle": "添加控制者",
932
-
"addControllerDescription": "添加一个可以在此账户上执行指定权限操作的用户。",
933
-
"controllerIdentifier": "控制者用户名或 DID",
934
-
"selectScopes": "选择权限级别",
935
-
"add": "添加",
936
"adding": "添加中...",
937
-
"cancel": "取消",
938
"accessLevel": "访问级别",
939
"addControllerButton": "+ 添加控制者",
940
"auditLogDesc": "查看所有委托活动",
···
234
"deleting": "删除中...",
235
"messages": {
236
"emailCodeSent": "验证码已发送到您的通知渠道",
237
+
"emailCodeSentToCurrent": "验证码已发送到您当前的邮箱地址",
238
"emailUpdated": "邮箱更新成功",
239
"emailUpdateFailed": "邮箱更新失败",
240
"handleUpdated": "用户名更新成功",
···
672
"continue": "继续",
673
"identifierLabel": "邮箱或标识符",
674
"identifierPlaceholder": "you@example.com",
675
+
"identifierHelp": "接收验证码的邮箱地址或标识符",
676
+
"backToSettings": "返回设置",
677
+
"emailUpdateCodeHelp": "验证码已发送到您当前的邮箱地址",
678
+
"emailUpdateFailed": "更新邮箱地址失败",
679
+
"emailUpdateRequiresAuth": "您需要登录才能更新邮箱地址。",
680
+
"emailUpdateSubtitle": "输入您的新邮箱地址和发送到当前邮箱的验证码。",
681
+
"emailUpdateTitle": "更新邮箱地址",
682
+
"emailUpdated": "您的邮箱地址已成功更新。",
683
+
"emailUpdatedInfo": "您可能需要验证新的邮箱地址。",
684
+
"newEmailLabel": "新邮箱地址",
685
+
"newEmailPlaceholder": "new@example.com",
686
+
"updateEmail": "更新邮箱",
687
+
"updating": "更新中..."
688
},
689
"resetPassword": {
690
"title": "重置密码",
···
834
"passkeysNotSupported": "此浏览器不支持通行密钥。请使用其他浏览器或使用密码注册。",
835
"passkeyCancelled": "通行密钥创建已取消",
836
"passkeyFailed": "通行密钥注册失败"
837
+
},
838
+
"didWebWarning1Detail": "您的身份将是 {did}。"
839
},
840
"trustedDevices": {
841
"title": "受信任设备",
···
893
"delegation": {
894
"title": "账户委托",
895
"controllers": "控制者",
896
"controlledAccounts": "受控账户",
897
"noControllers": "暂无控制者",
898
"noControlledAccounts": "无受控账户",
899
"addController": "添加控制者",
900
"handle": "用户名",
901
"did": "DID",
902
"scopeOwner": "所有者",
903
"scopeViewer": "查看者",
904
"scopeCustom": "自定义",
905
"actAs": "代理操作",
906
"auditLog": "审计日志",
907
"auditLogTitle": "委托审计日志",
···
925
"showing": "{start}–{end} / 共{total}条",
926
"refresh": "刷新",
927
"failedToLoadAuditLog": "加载审计日志失败",
928
"adding": "添加中...",
929
"accessLevel": "访问级别",
930
"addControllerButton": "+ 添加控制者",
931
"auditLogDesc": "查看所有委托活动",
+17
-23
frontend/src/routes/Settings.svelte
+17
-23
frontend/src/routes/Settings.svelte
···
56
if (message?.text === text) message = null
57
}, 5000)
58
}
59
-
async function handleRequestEmailUpdate(e: Event) {
60
-
e.preventDefault()
61
-
if (!auth.session || !newEmail) return
62
emailLoading = true
63
message = null
64
try {
65
-
const result = await api.requestEmailUpdate(auth.session.accessJwt, newEmail)
66
emailTokenRequired = result.tokenRequired
67
if (emailTokenRequired) {
68
-
showMessage('success', $_('settings.messages.emailCodeSent'))
69
} else {
70
-
await api.updateEmail(auth.session.accessJwt, newEmail)
71
-
await refreshSession()
72
-
showMessage('success', $_('settings.messages.emailUpdated'))
73
-
newEmail = ''
74
}
75
} catch (e) {
76
showMessage('error', e instanceof ApiError ? e.message : $_('settings.messages.emailUpdateFailed'))
···
244
required
245
/>
246
</div>
247
-
<div class="actions">
248
-
<button type="submit" disabled={emailLoading || !emailToken}>
249
-
{emailLoading ? $_('settings.updating') : $_('settings.confirmEmailChange')}
250
-
</button>
251
-
<button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = '' }}>
252
-
{$_('common.cancel')}
253
-
</button>
254
-
</div>
255
-
</form>
256
-
{:else}
257
-
<form onsubmit={handleRequestEmailUpdate}>
258
<div class="field">
259
<label for="new-email">{$_('settings.newEmail')}</label>
260
<input
···
266
required
267
/>
268
</div>
269
-
<button type="submit" disabled={emailLoading || !newEmail}>
270
-
{emailLoading ? $_('settings.requesting') : $_('settings.changeEmailButton')}
271
-
</button>
272
</form>
273
{/if}
274
</section>
275
<section>
···
56
if (message?.text === text) message = null
57
}, 5000)
58
}
59
+
async function handleRequestEmailUpdate() {
60
+
if (!auth.session) return
61
emailLoading = true
62
message = null
63
try {
64
+
const result = await api.requestEmailUpdate(auth.session.accessJwt)
65
emailTokenRequired = result.tokenRequired
66
if (emailTokenRequired) {
67
+
showMessage('success', $_('settings.messages.emailCodeSentToCurrent'))
68
} else {
69
+
emailTokenRequired = true
70
}
71
} catch (e) {
72
showMessage('error', e instanceof ApiError ? e.message : $_('settings.messages.emailUpdateFailed'))
···
240
required
241
/>
242
</div>
243
<div class="field">
244
<label for="new-email">{$_('settings.newEmail')}</label>
245
<input
···
251
required
252
/>
253
</div>
254
+
<div class="actions">
255
+
<button type="submit" disabled={emailLoading || !emailToken || !newEmail}>
256
+
{emailLoading ? $_('settings.updating') : $_('settings.confirmEmailChange')}
257
+
</button>
258
+
<button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = ''; newEmail = '' }}>
259
+
{$_('common.cancel')}
260
+
</button>
261
+
</div>
262
</form>
263
+
{:else}
264
+
<button onclick={handleRequestEmailUpdate} disabled={emailLoading}>
265
+
{emailLoading ? $_('settings.requesting') : $_('settings.changeEmailButton')}
266
+
</button>
267
{/if}
268
</section>
269
<section>
+94
-3
frontend/src/routes/Verify.svelte
+94
-3
frontend/src/routes/Verify.svelte
···
13
channel: string
14
}
15
16
-
type VerificationMode = 'signup' | 'token'
17
18
let mode = $state<VerificationMode>('signup')
19
let pendingVerification = $state<PendingVerification | null>(null)
20
let verificationCode = $state('')
21
let identifier = $state('')
···
50
onMount(async () => {
51
const params = parseQueryParams()
52
53
-
if (params.token) {
54
mode = 'token'
55
verificationCode = params.token
56
if (params.identifier) {
···
134
}
135
}
136
137
async function handleResendCode() {
138
if (mode === 'signup') {
139
if (!pendingVerification || resendingCode) return
···
198
{:else if success}
199
<div class="success-container">
200
<h1>{$_('verify.verified')}</h1>
201
-
{#if successPurpose === 'migration' || successPurpose === 'signup'}
202
<p class="subtitle">{$_('verify.channelVerified', { values: { channel: channelLabel(successChannel || '') } })}</p>
203
<p class="info-text">{$_('verify.canNowSignIn')}</p>
204
<div class="actions">
···
213
</div>
214
{/if}
215
</div>
216
{:else if mode === 'token'}
217
<h1>{$_('verify.tokenTitle')}</h1>
218
<p class="subtitle">{$_('verify.tokenSubtitle')}</p>
···
13
channel: string
14
}
15
16
+
type VerificationMode = 'signup' | 'token' | 'email-update'
17
18
let mode = $state<VerificationMode>('signup')
19
+
let newEmail = $state('')
20
let pendingVerification = $state<PendingVerification | null>(null)
21
let verificationCode = $state('')
22
let identifier = $state('')
···
51
onMount(async () => {
52
const params = parseQueryParams()
53
54
+
if (params.type === 'email-update') {
55
+
mode = 'email-update'
56
+
if (params.token) {
57
+
verificationCode = params.token
58
+
}
59
+
} else if (params.token) {
60
mode = 'token'
61
verificationCode = params.token
62
if (params.identifier) {
···
140
}
141
}
142
143
+
async function handleEmailUpdate() {
144
+
if (!verificationCode.trim() || !newEmail.trim()) return
145
+
146
+
if (!auth.session) {
147
+
error = $_('verify.emailUpdateRequiresAuth')
148
+
return
149
+
}
150
+
151
+
submitting = true
152
+
error = null
153
+
154
+
try {
155
+
await api.updateEmail(auth.session.accessJwt, newEmail.trim(), verificationCode.trim())
156
+
success = true
157
+
successPurpose = 'email-update'
158
+
successChannel = 'email'
159
+
} catch (e: any) {
160
+
if (e instanceof ApiError) {
161
+
error = e.message
162
+
} else {
163
+
error = $_('verify.emailUpdateFailed')
164
+
}
165
+
} finally {
166
+
submitting = false
167
+
}
168
+
}
169
+
170
async function handleResendCode() {
171
if (mode === 'signup') {
172
if (!pendingVerification || resendingCode) return
···
231
{:else if success}
232
<div class="success-container">
233
<h1>{$_('verify.verified')}</h1>
234
+
{#if successPurpose === 'email-update'}
235
+
<p class="subtitle">{$_('verify.emailUpdated')}</p>
236
+
<p class="info-text">{$_('verify.emailUpdatedInfo')}</p>
237
+
<div class="actions">
238
+
<a href="#/settings" class="btn">{$_('verify.backToSettings')}</a>
239
+
</div>
240
+
{:else if successPurpose === 'migration' || successPurpose === 'signup'}
241
<p class="subtitle">{$_('verify.channelVerified', { values: { channel: channelLabel(successChannel || '') } })}</p>
242
<p class="info-text">{$_('verify.canNowSignIn')}</p>
243
<div class="actions">
···
252
</div>
253
{/if}
254
</div>
255
+
{:else if mode === 'email-update'}
256
+
<h1>{$_('verify.emailUpdateTitle')}</h1>
257
+
<p class="subtitle">{$_('verify.emailUpdateSubtitle')}</p>
258
+
259
+
{#if !auth.session}
260
+
<div class="message warning">{$_('verify.emailUpdateRequiresAuth')}</div>
261
+
<div class="actions">
262
+
<a href="#/login" class="btn">{$_('verify.signIn')}</a>
263
+
</div>
264
+
{:else}
265
+
{#if error}
266
+
<div class="message error">{error}</div>
267
+
{/if}
268
+
269
+
<form onsubmit={(e) => { e.preventDefault(); handleEmailUpdate(); }}>
270
+
<div class="field">
271
+
<label for="new-email">{$_('verify.newEmailLabel')}</label>
272
+
<input
273
+
id="new-email"
274
+
type="email"
275
+
bind:value={newEmail}
276
+
placeholder={$_('verify.newEmailPlaceholder')}
277
+
disabled={submitting}
278
+
required
279
+
autocomplete="email"
280
+
/>
281
+
</div>
282
+
283
+
<div class="field">
284
+
<label for="verification-code">{$_('verify.codeLabel')}</label>
285
+
<input
286
+
id="verification-code"
287
+
type="text"
288
+
bind:value={verificationCode}
289
+
placeholder={$_('verify.codePlaceholder')}
290
+
disabled={submitting}
291
+
required
292
+
autocomplete="off"
293
+
class="token-input"
294
+
/>
295
+
<p class="field-help">{$_('verify.emailUpdateCodeHelp')}</p>
296
+
</div>
297
+
298
+
<button type="submit" disabled={submitting || !verificationCode.trim() || !newEmail.trim()}>
299
+
{submitting ? $_('verify.updating') : $_('verify.updateEmail')}
300
+
</button>
301
+
</form>
302
+
303
+
<p class="link-text">
304
+
<a href="#/settings">{$_('verify.backToSettings')}</a>
305
+
</p>
306
+
{/if}
307
{:else if mode === 'token'}
308
<h1>{$_('verify.tokenTitle')}</h1>
309
<p class="subtitle">{$_('verify.tokenSubtitle')}</p>
+200
-175
src/api/server/email.rs
+200
-175
src/api/server/email.rs
···
1
use crate::api::ApiError;
2
use crate::state::{AppState, RateLimitKind};
3
use axum::{
4
Json,
···
10
use serde_json::json;
11
use tracing::{error, info, warn};
12
13
-
#[derive(Deserialize)]
14
-
#[serde(rename_all = "camelCase")]
15
-
pub struct RequestEmailUpdateInput {
16
-
pub email: String,
17
-
}
18
-
19
pub async fn request_email_update(
20
State(state): State<AppState>,
21
headers: axum::http::HeaderMap,
22
-
Json(input): Json<RequestEmailUpdateInput>,
23
) -> Response {
24
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
25
if !state
···
37
.into_response();
38
}
39
40
-
let token = match crate::auth::extract_bearer_token_from_header(
41
-
headers.get("Authorization").and_then(|h| h.to_str().ok()),
42
-
) {
43
-
Some(t) => t,
44
-
None => {
45
-
return (
46
-
StatusCode::UNAUTHORIZED,
47
-
Json(json!({"error": "AuthenticationRequired"})),
48
-
)
49
-
.into_response();
50
-
}
51
-
};
52
-
53
-
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
54
-
let auth_user = match auth_result {
55
-
Ok(user) => user,
56
-
Err(e) => return ApiError::from(e).into_response(),
57
-
};
58
-
59
if let Err(e) = crate::auth::scope_check::check_account_scope(
60
-
auth_user.is_oauth,
61
-
auth_user.scope.as_deref(),
62
crate::oauth::scopes::AccountAttr::Email,
63
crate::oauth::scopes::AccountAction::Manage,
64
) {
65
return e;
66
}
67
68
-
let did = auth_user.did.clone();
69
-
let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did)
70
-
.fetch_optional(&state.db)
71
-
.await
72
{
73
Ok(Some(row)) => row,
74
-
_ => {
75
return (
76
StatusCode::INTERNAL_SERVER_ERROR,
77
Json(json!({"error": "InternalError"})),
···
80
}
81
};
82
83
-
let user_id = user.id;
84
-
let handle = user.handle;
85
-
let current_email = user.email;
86
-
let email = input.email.trim().to_lowercase();
87
88
-
if !crate::api::validation::is_valid_email(&email) {
89
-
return (
90
-
StatusCode::BAD_REQUEST,
91
-
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
92
-
)
93
-
.into_response();
94
-
}
95
96
-
if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) {
97
-
return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response();
98
-
}
99
-
100
-
let exists = sqlx::query!(
101
-
"SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
102
-
email,
103
-
user_id
104
-
)
105
-
.fetch_optional(&state.db)
106
-
.await;
107
-
108
-
if let Ok(Some(_)) = exists {
109
-
return (
110
-
StatusCode::BAD_REQUEST,
111
-
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
112
-
)
113
-
.into_response();
114
-
}
115
116
-
if let Err(e) = crate::api::notification_prefs::request_channel_verification(
117
-
&state.db,
118
-
user_id,
119
-
&did,
120
-
"email",
121
-
&email,
122
-
Some(&handle),
123
-
)
124
-
.await
125
-
{
126
-
error!("Failed to request email verification: {}", e);
127
-
return (
128
-
StatusCode::INTERNAL_SERVER_ERROR,
129
-
Json(json!({"error": "InternalError"})),
130
)
131
-
.into_response();
132
}
133
134
-
info!("Email update requested for user {}", user_id);
135
-
(StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
136
}
137
138
#[derive(Deserialize)]
···
145
pub async fn confirm_email(
146
State(state): State<AppState>,
147
headers: axum::http::HeaderMap,
148
Json(input): Json<ConfirmEmailInput>,
149
) -> Response {
150
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
151
if !state
152
-
.check_rate_limit(RateLimitKind::AppPassword, &client_ip)
153
.await
154
{
155
warn!(ip = %client_ip, "Confirm email rate limit exceeded");
···
163
.into_response();
164
}
165
166
-
let token = match crate::auth::extract_bearer_token_from_header(
167
-
headers.get("Authorization").and_then(|h| h.to_str().ok()),
168
-
) {
169
-
Some(t) => t,
170
-
None => {
171
-
return (
172
-
StatusCode::UNAUTHORIZED,
173
-
Json(json!({"error": "AuthenticationRequired"})),
174
-
)
175
-
.into_response();
176
-
}
177
-
};
178
-
179
-
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
180
-
let auth_user = match auth_result {
181
-
Ok(user) => user,
182
-
Err(e) => return ApiError::from(e).into_response(),
183
-
};
184
-
185
if let Err(e) = crate::auth::scope_check::check_account_scope(
186
-
auth_user.is_oauth,
187
-
auth_user.scope.as_deref(),
188
crate::oauth::scopes::AccountAttr::Email,
189
crate::oauth::scopes::AccountAction::Manage,
190
) {
191
return e;
192
}
193
194
-
let did = auth_user.did;
195
-
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
196
-
.fetch_one(&state.db)
197
-
.await
198
{
199
-
Ok(id) => id,
200
-
Err(_) => {
201
return (
202
StatusCode::INTERNAL_SERVER_ERROR,
203
Json(json!({"error": "InternalError"})),
···
206
}
207
};
208
209
-
let email = input.email.trim().to_lowercase();
210
let confirmation_code =
211
crate::auth::verification_token::normalize_token_input(input.token.trim());
212
213
-
let verified = crate::auth::verification_token::verify_channel_update_token(
214
&confirmation_code,
215
"email",
216
-
&email,
217
);
218
219
match verified {
···
245
}
246
247
let update = sqlx::query!(
248
-
"UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
249
-
email,
250
-
user_id
251
)
252
.execute(&state.db)
253
.await;
254
255
if let Err(e) = update {
256
-
error!("DB error finalizing email update: {:?}", e);
257
-
if e.as_database_error()
258
-
.map(|db_err| db_err.is_unique_violation())
259
-
.unwrap_or(false)
260
-
{
261
-
return (
262
-
StatusCode::BAD_REQUEST,
263
-
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
264
-
)
265
-
.into_response();
266
-
}
267
return (
268
StatusCode::INTERNAL_SERVER_ERROR,
269
Json(json!({"error": "InternalError"})),
···
271
.into_response();
272
}
273
274
-
info!("Email updated for user {}", user_id);
275
(StatusCode::OK, Json(json!({}))).into_response()
276
}
277
···
289
headers: axum::http::HeaderMap,
290
Json(input): Json<UpdateEmailInput>,
291
) -> Response {
292
-
let token = match crate::auth::extract_bearer_token_from_header(
293
headers.get("Authorization").and_then(|h| h.to_str().ok()),
294
) {
295
Some(t) => t,
···
302
}
303
};
304
305
-
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
306
let auth_user = match auth_result {
307
Ok(user) => user,
308
Err(e) => return ApiError::from(e).into_response(),
···
318
}
319
320
let did = auth_user.did;
321
-
let user = match sqlx::query!("SELECT id, email FROM users WHERE did = $1", did)
322
-
.fetch_optional(&state.db)
323
-
.await
324
{
325
Ok(Some(row)) => row,
326
-
_ => {
327
return (
328
StatusCode::INTERNAL_SERVER_ERROR,
329
Json(json!({"error": "InternalError"})),
···
333
};
334
335
let user_id = user.id;
336
-
let current_email = user.email;
337
let new_email = input.email.trim().to_lowercase();
338
339
if !crate::api::validation::is_valid_email(&new_email) {
340
return (
341
StatusCode::BAD_REQUEST,
342
-
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
343
)
344
.into_response();
345
}
···
350
return (StatusCode::OK, Json(json!({}))).into_response();
351
}
352
353
-
let confirmation_token = match &input.token {
354
-
Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
355
-
None => {
356
-
return (
357
-
StatusCode::BAD_REQUEST,
358
-
Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})),
359
-
)
360
-
.into_response();
361
-
}
362
-
};
363
364
-
let verified = crate::auth::verification_token::verify_channel_update_token(
365
-
&confirmation_token,
366
-
"email",
367
-
&new_email,
368
-
);
369
370
-
match verified {
371
-
Ok(token_data) => {
372
-
if token_data.did != did {
373
return (
374
StatusCode::BAD_REQUEST,
375
-
Json(
376
-
json!({"error": "InvalidToken", "message": "Token does not match account"}),
377
-
),
378
)
379
.into_response();
380
}
381
}
382
-
Err(crate::auth::verification_token::VerifyError::Expired) => {
383
-
return (
384
-
StatusCode::BAD_REQUEST,
385
-
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
386
-
)
387
-
.into_response();
388
-
}
389
-
Err(_) => {
390
-
return (
391
-
StatusCode::BAD_REQUEST,
392
-
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
393
-
)
394
-
.into_response();
395
-
}
396
}
397
398
let exists = sqlx::query!(
···
406
if let Ok(Some(_)) = exists {
407
return (
408
StatusCode::BAD_REQUEST,
409
-
Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
410
)
411
.into_response();
412
}
413
414
-
let update = sqlx::query!(
415
-
"UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
416
new_email,
417
user_id
418
)
···
420
.await;
421
422
if let Err(e) = update {
423
-
error!("DB error finalizing email update: {:?}", e);
424
if e.as_database_error()
425
-
.map(|db_err| db_err.is_unique_violation())
426
.unwrap_or(false)
427
{
428
return (
429
StatusCode::BAD_REQUEST,
430
-
Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
431
)
432
.into_response();
433
}
···
436
Json(json!({"error": "InternalError"})),
437
)
438
.into_response();
439
}
440
441
match sqlx::query!(
···
1
use crate::api::ApiError;
2
+
use crate::auth::BearerAuth;
3
use crate::state::{AppState, RateLimitKind};
4
use axum::{
5
Json,
···
11
use serde_json::json;
12
use tracing::{error, info, warn};
13
14
pub async fn request_email_update(
15
State(state): State<AppState>,
16
headers: axum::http::HeaderMap,
17
+
auth: BearerAuth,
18
) -> Response {
19
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
20
if !state
···
32
.into_response();
33
}
34
35
if let Err(e) = crate::auth::scope_check::check_account_scope(
36
+
auth.0.is_oauth,
37
+
auth.0.scope.as_deref(),
38
crate::oauth::scopes::AccountAttr::Email,
39
crate::oauth::scopes::AccountAction::Manage,
40
) {
41
return e;
42
}
43
44
+
let did = auth.0.did.clone();
45
+
let user = match sqlx::query!(
46
+
"SELECT id, handle, email, email_verified FROM users WHERE did = $1",
47
+
did
48
+
)
49
+
.fetch_optional(&state.db)
50
+
.await
51
{
52
Ok(Some(row)) => row,
53
+
Ok(None) => {
54
+
return (
55
+
StatusCode::BAD_REQUEST,
56
+
Json(json!({"error": "InvalidRequest", "message": "account not found"})),
57
+
)
58
+
.into_response();
59
+
}
60
+
Err(e) => {
61
+
error!("DB error: {:?}", e);
62
return (
63
StatusCode::INTERNAL_SERVER_ERROR,
64
Json(json!({"error": "InternalError"})),
···
67
}
68
};
69
70
+
let current_email: String = match user.email {
71
+
Some(e) => e,
72
+
None => {
73
+
return (
74
+
StatusCode::BAD_REQUEST,
75
+
Json(json!({"error": "InvalidRequest", "message": "account does not have an email address"})),
76
+
)
77
+
.into_response();
78
+
}
79
+
};
80
81
+
let token_required = user.email_verified;
82
83
+
if token_required {
84
+
let code = crate::auth::verification_token::generate_channel_update_token(
85
+
&did,
86
+
"email_update",
87
+
¤t_email.to_lowercase(),
88
+
);
89
+
let formatted_code =
90
+
crate::auth::verification_token::format_token_for_display(&code);
91
92
+
let hostname =
93
+
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
94
+
if let Err(e) = crate::comms::enqueue_email_update_token(
95
+
&state.db,
96
+
user.id,
97
+
&formatted_code,
98
+
&hostname,
99
)
100
+
.await
101
+
{
102
+
warn!("Failed to enqueue email update notification: {:?}", e);
103
+
}
104
}
105
106
+
info!("Email update requested for user {}", user.id);
107
+
(StatusCode::OK, Json(json!({ "tokenRequired": token_required }))).into_response()
108
}
109
110
#[derive(Deserialize)]
···
117
pub async fn confirm_email(
118
State(state): State<AppState>,
119
headers: axum::http::HeaderMap,
120
+
auth: BearerAuth,
121
Json(input): Json<ConfirmEmailInput>,
122
) -> Response {
123
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
124
if !state
125
+
.check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
126
.await
127
{
128
warn!(ip = %client_ip, "Confirm email rate limit exceeded");
···
136
.into_response();
137
}
138
139
if let Err(e) = crate::auth::scope_check::check_account_scope(
140
+
auth.0.is_oauth,
141
+
auth.0.scope.as_deref(),
142
crate::oauth::scopes::AccountAttr::Email,
143
crate::oauth::scopes::AccountAction::Manage,
144
) {
145
return e;
146
}
147
148
+
let did = auth.0.did;
149
+
let user = match sqlx::query!(
150
+
"SELECT id, email, email_verified FROM users WHERE did = $1",
151
+
did
152
+
)
153
+
.fetch_optional(&state.db)
154
+
.await
155
{
156
+
Ok(Some(row)) => row,
157
+
Ok(None) => {
158
+
return (
159
+
StatusCode::BAD_REQUEST,
160
+
Json(json!({"error": "AccountNotFound", "message": "user not found"})),
161
+
)
162
+
.into_response();
163
+
}
164
+
Err(e) => {
165
+
error!("DB error: {:?}", e);
166
return (
167
StatusCode::INTERNAL_SERVER_ERROR,
168
Json(json!({"error": "InternalError"})),
···
171
}
172
};
173
174
+
let current_email = match &user.email {
175
+
Some(e) => e.to_lowercase(),
176
+
None => {
177
+
return (
178
+
StatusCode::BAD_REQUEST,
179
+
Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})),
180
+
)
181
+
.into_response();
182
+
}
183
+
};
184
+
185
+
let provided_email = input.email.trim().to_lowercase();
186
+
if provided_email != current_email {
187
+
return (
188
+
StatusCode::BAD_REQUEST,
189
+
Json(json!({"error": "InvalidEmail", "message": "invalid email"})),
190
+
)
191
+
.into_response();
192
+
}
193
+
194
+
if user.email_verified {
195
+
return (StatusCode::OK, Json(json!({}))).into_response();
196
+
}
197
+
198
let confirmation_code =
199
crate::auth::verification_token::normalize_token_input(input.token.trim());
200
201
+
let verified = crate::auth::verification_token::verify_signup_token(
202
&confirmation_code,
203
"email",
204
+
&provided_email,
205
);
206
207
match verified {
···
233
}
234
235
let update = sqlx::query!(
236
+
"UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
237
+
user.id
238
)
239
.execute(&state.db)
240
.await;
241
242
if let Err(e) = update {
243
+
error!("DB error confirming email: {:?}", e);
244
return (
245
StatusCode::INTERNAL_SERVER_ERROR,
246
Json(json!({"error": "InternalError"})),
···
248
.into_response();
249
}
250
251
+
info!("Email confirmed for user {}", user.id);
252
(StatusCode::OK, Json(json!({}))).into_response()
253
}
254
···
266
headers: axum::http::HeaderMap,
267
Json(input): Json<UpdateEmailInput>,
268
) -> Response {
269
+
let bearer_token = match crate::auth::extract_bearer_token_from_header(
270
headers.get("Authorization").and_then(|h| h.to_str().ok()),
271
) {
272
Some(t) => t,
···
279
}
280
};
281
282
+
let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await;
283
let auth_user = match auth_result {
284
Ok(user) => user,
285
Err(e) => return ApiError::from(e).into_response(),
···
295
}
296
297
let did = auth_user.did;
298
+
let user = match sqlx::query!(
299
+
"SELECT id, email, email_verified FROM users WHERE did = $1",
300
+
did
301
+
)
302
+
.fetch_optional(&state.db)
303
+
.await
304
{
305
Ok(Some(row)) => row,
306
+
Ok(None) => {
307
+
return (
308
+
StatusCode::BAD_REQUEST,
309
+
Json(json!({"error": "InvalidRequest", "message": "account not found"})),
310
+
)
311
+
.into_response();
312
+
}
313
+
Err(e) => {
314
+
error!("DB error: {:?}", e);
315
return (
316
StatusCode::INTERNAL_SERVER_ERROR,
317
Json(json!({"error": "InternalError"})),
···
321
};
322
323
let user_id = user.id;
324
+
let current_email = user.email.clone();
325
+
let email_verified = user.email_verified;
326
let new_email = input.email.trim().to_lowercase();
327
328
if !crate::api::validation::is_valid_email(&new_email) {
329
return (
330
StatusCode::BAD_REQUEST,
331
+
Json(json!({
332
+
"error": "InvalidRequest",
333
+
"message": "This email address is not supported, please use a different email."
334
+
})),
335
)
336
.into_response();
337
}
···
342
return (StatusCode::OK, Json(json!({}))).into_response();
343
}
344
345
+
if email_verified {
346
+
let confirmation_token = match &input.token {
347
+
Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
348
+
None => {
349
+
return (
350
+
StatusCode::BAD_REQUEST,
351
+
Json(json!({
352
+
"error": "TokenRequired",
353
+
"message": "confirmation token required"
354
+
})),
355
+
)
356
+
.into_response();
357
+
}
358
+
};
359
+
360
+
let current_email_lower = current_email
361
+
.as_ref()
362
+
.map(|e| e.to_lowercase())
363
+
.unwrap_or_default();
364
365
+
let verified = crate::auth::verification_token::verify_channel_update_token(
366
+
&confirmation_token,
367
+
"email_update",
368
+
¤t_email_lower,
369
+
);
370
371
+
match verified {
372
+
Ok(token_data) => {
373
+
if token_data.did != did {
374
+
return (
375
+
StatusCode::BAD_REQUEST,
376
+
Json(
377
+
json!({"error": "InvalidToken", "message": "Token does not match account"}),
378
+
),
379
+
)
380
+
.into_response();
381
+
}
382
+
}
383
+
Err(crate::auth::verification_token::VerifyError::Expired) => {
384
+
return (
385
+
StatusCode::BAD_REQUEST,
386
+
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
387
+
)
388
+
.into_response();
389
+
}
390
+
Err(_) => {
391
return (
392
StatusCode::BAD_REQUEST,
393
+
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
394
)
395
.into_response();
396
}
397
}
398
}
399
400
let exists = sqlx::query!(
···
408
if let Ok(Some(_)) = exists {
409
return (
410
StatusCode::BAD_REQUEST,
411
+
Json(json!({
412
+
"error": "InvalidRequest",
413
+
"message": "This email address is already in use, please use a different email."
414
+
})),
415
)
416
.into_response();
417
}
418
419
+
let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
420
+
"UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
421
new_email,
422
user_id
423
)
···
425
.await;
426
427
if let Err(e) = update {
428
+
error!("DB error updating email: {:?}", e);
429
if e.as_database_error()
430
+
.map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
431
.unwrap_or(false)
432
{
433
return (
434
StatusCode::BAD_REQUEST,
435
+
Json(json!({
436
+
"error": "InvalidRequest",
437
+
"message": "This email address is already in use, please use a different email."
438
+
})),
439
)
440
.into_response();
441
}
···
444
Json(json!({"error": "InternalError"})),
445
)
446
.into_response();
447
+
}
448
+
449
+
let verification_token =
450
+
crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
451
+
let formatted_token =
452
+
crate::auth::verification_token::format_token_for_display(&verification_token);
453
+
if let Err(e) = crate::comms::enqueue_signup_verification(
454
+
&state.db,
455
+
user_id,
456
+
"email",
457
+
&new_email,
458
+
&formatted_token,
459
+
None,
460
+
)
461
+
.await
462
+
{
463
+
warn!("Failed to send verification email to new address: {:?}", e);
464
}
465
466
match sqlx::query!(
+3
-3
src/comms/mod.rs
+3
-3
src/comms/mod.rs
···
10
11
pub use service::{
12
CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion, enqueue_comms,
13
-
enqueue_email_update, enqueue_migration_verification, enqueue_passkey_recovery,
14
-
enqueue_password_reset, enqueue_plc_operation, enqueue_signup_verification, enqueue_welcome,
15
-
queue_legacy_login_notification,
16
};
17
18
pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
···
10
11
pub use service::{
12
CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion, enqueue_comms,
13
+
enqueue_email_update, enqueue_email_update_token, enqueue_migration_verification,
14
+
enqueue_passkey_recovery, enqueue_password_reset, enqueue_plc_operation,
15
+
enqueue_signup_verification, enqueue_welcome, queue_legacy_login_notification,
16
};
17
18
pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
+38
src/comms/service.rs
+38
src/comms/service.rs
···
380
.await
381
}
382
383
+
pub async fn enqueue_email_update_token(
384
+
db: &PgPool,
385
+
user_id: Uuid,
386
+
code: &str,
387
+
hostname: &str,
388
+
) -> Result<Uuid, sqlx::Error> {
389
+
let prefs = get_user_comms_prefs(db, user_id).await?;
390
+
let strings = get_strings(&prefs.locale);
391
+
let current_email = prefs.email.clone().unwrap_or_default();
392
+
let verify_page = format!("https://{}/#/verify?type=email-update", hostname);
393
+
let verify_link = format!(
394
+
"https://{}/#/verify?type=email-update&token={}",
395
+
hostname,
396
+
urlencoding::encode(code)
397
+
);
398
+
let body = format_message(
399
+
strings.email_update_body,
400
+
&[
401
+
("handle", &prefs.handle),
402
+
("code", code),
403
+
("verify_page", &verify_page),
404
+
("verify_link", &verify_link),
405
+
],
406
+
);
407
+
let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]);
408
+
enqueue_comms(
409
+
db,
410
+
NewComms::email(
411
+
user_id,
412
+
super::types::CommsType::EmailUpdate,
413
+
current_email,
414
+
subject,
415
+
body,
416
+
),
417
+
)
418
+
.await
419
+
}
420
+
421
pub async fn enqueue_account_deletion(
422
db: &PgPool,
423
user_id: Uuid,
+255
-158
tests/email_update.rs
+255
-158
tests/email_update.rs
···
63
}
64
65
#[tokio::test]
66
-
async fn test_email_update_flow_success() {
67
let client = common::client();
68
let base_url = common::base_url().await;
69
let pool = get_pool().await;
···
71
let email = format!("{}@example.com", handle);
72
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
73
let new_email = format!("new_{}@example.com", handle);
74
let res = client
75
.post(format!(
76
"{}/xrpc/com.atproto.server.requestEmailUpdate",
77
base_url
78
))
79
.bearer_auth(&access_jwt)
80
-
.json(&json!({"email": new_email}))
81
.send()
82
.await
83
.expect("Failed to request email update");
···
86
assert_eq!(body["tokenRequired"], true);
87
88
let code = get_email_update_token(&pool, &did).await;
89
let res = client
90
-
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
91
.bearer_auth(&access_jwt)
92
.json(&json!({
93
"email": new_email,
···
95
}))
96
.send()
97
.await
98
-
.expect("Failed to confirm email");
99
assert_eq!(res.status(), StatusCode::OK);
100
-
let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
101
.fetch_one(&pool)
102
.await
103
.expect("User not found");
104
-
assert_eq!(user.email, Some(new_email));
105
}
106
107
#[tokio::test]
108
-
async fn test_request_email_update_taken_email() {
109
let client = common::client();
110
let base_url = common::base_url().await;
111
-
let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4());
112
-
let email1 = format!("{}@example.com", handle1);
113
-
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
114
-
let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4());
115
-
let email2 = format!("{}@example.com", handle2);
116
-
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
117
let res = client
118
-
.post(format!(
119
-
"{}/xrpc/com.atproto.server.requestEmailUpdate",
120
-
base_url
121
-
))
122
-
.bearer_auth(&access_jwt2)
123
-
.json(&json!({"email": email1}))
124
.send()
125
.await
126
-
.expect("Failed to request email update");
127
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
128
let body: Value = res.json().await.expect("Invalid JSON");
129
-
assert_eq!(body["error"], "EmailTaken");
130
}
131
132
#[tokio::test]
133
-
async fn test_confirm_email_invalid_token() {
134
let client = common::client();
135
let base_url = common::base_url().await;
136
-
let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4());
137
let email = format!("{}@example.com", handle);
138
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
139
-
let new_email = format!("new_{}@example.com", handle);
140
-
let res = client
141
-
.post(format!(
142
-
"{}/xrpc/com.atproto.server.requestEmailUpdate",
143
-
base_url
144
-
))
145
-
.bearer_auth(&access_jwt)
146
-
.json(&json!({"email": new_email}))
147
-
.send()
148
-
.await
149
-
.expect("Failed to request email update");
150
-
assert_eq!(res.status(), StatusCode::OK);
151
let res = client
152
-
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
153
.bearer_auth(&access_jwt)
154
-
.json(&json!({
155
-
"email": new_email,
156
-
"token": "wrong-token"
157
-
}))
158
.send()
159
.await
160
-
.expect("Failed to confirm email");
161
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
162
-
let body: Value = res.json().await.expect("Invalid JSON");
163
-
assert_eq!(body["error"], "InvalidToken");
164
}
165
166
#[tokio::test]
167
-
async fn test_confirm_email_wrong_email() {
168
let client = common::client();
169
let base_url = common::base_url().await;
170
-
let pool = get_pool().await;
171
-
let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4());
172
let email = format!("{}@example.com", handle);
173
-
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
174
-
let new_email = format!("new_{}@example.com", handle);
175
let res = client
176
.post(format!(
177
"{}/xrpc/com.atproto.server.requestEmailUpdate",
178
base_url
179
))
180
.bearer_auth(&access_jwt)
181
-
.json(&json!({"email": new_email}))
182
.send()
183
.await
184
.expect("Failed to request email update");
185
assert_eq!(res.status(), StatusCode::OK);
186
-
let code = get_email_update_token(&pool, &did).await;
187
let res = client
188
-
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
189
.bearer_auth(&access_jwt)
190
.json(&json!({
191
-
"email": "another_random@example.com",
192
-
"token": code
193
}))
194
.send()
195
.await
196
-
.expect("Failed to confirm email");
197
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
198
let body: Value = res.json().await.expect("Invalid JSON");
199
-
assert!(
200
-
body["message"].as_str().unwrap().contains("mismatch") || body["error"] == "InvalidToken"
201
-
);
202
}
203
204
#[tokio::test]
205
-
async fn test_update_email_requires_token() {
206
let client = common::client();
207
let base_url = common::base_url().await;
208
-
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
209
-
let email = format!("{}@example.com", handle);
210
-
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
211
-
let new_email = format!("direct_{}@example.com", handle);
212
let res = client
213
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
214
-
.bearer_auth(&access_jwt)
215
-
.json(&json!({ "email": new_email }))
216
.send()
217
.await
218
-
.expect("Failed to update email");
219
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
220
let body: Value = res.json().await.expect("Invalid JSON");
221
-
assert_eq!(body["error"], "TokenRequired");
222
}
223
224
#[tokio::test]
225
-
async fn test_update_email_same_email_noop() {
226
let client = common::client();
227
let base_url = common::base_url().await;
228
-
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
229
let email = format!("{}@example.com", handle);
230
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
231
let res = client
232
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
233
.bearer_auth(&access_jwt)
234
-
.json(&json!({ "email": email }))
235
.send()
236
.await
237
-
.expect("Failed to update email");
238
-
assert_eq!(
239
-
res.status(),
240
-
StatusCode::OK,
241
-
"Updating to same email should succeed as no-op"
242
-
);
243
}
244
245
#[tokio::test]
246
-
async fn test_update_email_requires_token_after_pending() {
247
let client = common::client();
248
let base_url = common::base_url().await;
249
-
let handle = format!("emailup-token-{}", uuid::Uuid::new_v4());
250
let email = format!("{}@example.com", handle);
251
-
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
252
-
let new_email = format!("pending_{}@example.com", handle);
253
let res = client
254
.post(format!(
255
-
"{}/xrpc/com.atproto.server.requestEmailUpdate",
256
base_url
257
))
258
-
.bearer_auth(&access_jwt)
259
-
.json(&json!({"email": new_email}))
260
.send()
261
.await
262
-
.expect("Failed to request email update");
263
assert_eq!(res.status(), StatusCode::OK);
264
let res = client
265
-
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
266
.bearer_auth(&access_jwt)
267
-
.json(&json!({ "email": new_email }))
268
.send()
269
.await
270
-
.expect("Failed to attempt email update");
271
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
272
-
let body: Value = res.json().await.expect("Invalid JSON");
273
-
assert_eq!(body["error"], "TokenRequired");
274
}
275
276
#[tokio::test]
277
-
async fn test_update_email_with_valid_token() {
278
let client = common::client();
279
let base_url = common::base_url().await;
280
let pool = get_pool().await;
281
-
let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4());
282
let email = format!("{}@example.com", handle);
283
-
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
284
-
let new_email = format!("valid_{}@example.com", handle);
285
let res = client
286
.post(format!(
287
-
"{}/xrpc/com.atproto.server.requestEmailUpdate",
288
base_url
289
))
290
-
.bearer_auth(&access_jwt)
291
-
.json(&json!({"email": new_email}))
292
.send()
293
.await
294
-
.expect("Failed to request email update");
295
assert_eq!(res.status(), StatusCode::OK);
296
-
let code = get_email_update_token(&pool, &did).await;
297
let res = client
298
-
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
299
.bearer_auth(&access_jwt)
300
.json(&json!({
301
-
"email": new_email,
302
"token": code
303
}))
304
.send()
305
.await
306
-
.expect("Failed to update email");
307
-
assert_eq!(res.status(), StatusCode::OK);
308
-
let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
309
-
.fetch_one(&pool)
310
-
.await
311
-
.expect("User not found");
312
-
assert_eq!(user.email, Some(new_email));
313
}
314
315
#[tokio::test]
316
-
async fn test_update_email_invalid_token() {
317
let client = common::client();
318
let base_url = common::base_url().await;
319
-
let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4());
320
let email = format!("{}@example.com", handle);
321
-
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
322
-
let new_email = format!("badtok_{}@example.com", handle);
323
let res = client
324
.post(format!(
325
-
"{}/xrpc/com.atproto.server.requestEmailUpdate",
326
base_url
327
))
328
-
.bearer_auth(&access_jwt)
329
-
.json(&json!({"email": new_email}))
330
.send()
331
.await
332
-
.expect("Failed to request email update");
333
assert_eq!(res.status(), StatusCode::OK);
334
let res = client
335
-
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
336
.bearer_auth(&access_jwt)
337
.json(&json!({
338
-
"email": new_email,
339
-
"token": "wrong-token-12345"
340
}))
341
.send()
342
.await
343
-
.expect("Failed to attempt email update");
344
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
345
let body: Value = res.json().await.expect("Invalid JSON");
346
assert_eq!(body["error"], "InvalidToken");
347
}
348
349
#[tokio::test]
350
-
async fn test_update_email_already_taken() {
351
let client = common::client();
352
let base_url = common::base_url().await;
353
-
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
354
-
let email1 = format!("{}@example.com", handle1);
355
-
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
356
-
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
357
-
let email2 = format!("{}@example.com", handle2);
358
-
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
359
let res = client
360
-
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
361
-
.bearer_auth(&access_jwt2)
362
-
.json(&json!({ "email": email1 }))
363
.send()
364
.await
365
-
.expect("Failed to attempt email update");
366
-
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
367
let body: Value = res.json().await.expect("Invalid JSON");
368
-
assert!(
369
-
body["error"] == "TokenRequired"
370
-
|| body["message"]
371
-
.as_str()
372
-
.unwrap_or("")
373
-
.contains("already in use")
374
-
|| body["error"] == "InvalidRequest"
375
);
376
-
}
377
378
-
#[tokio::test]
379
-
async fn test_update_email_no_auth() {
380
-
let client = common::client();
381
-
let base_url = common::base_url().await;
382
let res = client
383
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
384
-
.json(&json!({ "email": "test@example.com" }))
385
.send()
386
.await
387
-
.expect("Failed to send request");
388
-
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
389
-
let body: Value = res.json().await.expect("Invalid JSON");
390
-
assert_eq!(body["error"], "AuthenticationRequired");
391
}
392
393
#[tokio::test]
394
-
async fn test_update_email_invalid_format() {
395
let client = common::client();
396
let base_url = common::base_url().await;
397
-
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
398
-
let email = format!("{}@example.com", handle);
399
-
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
400
let res = client
401
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
402
-
.bearer_auth(&access_jwt)
403
-
.json(&json!({ "email": "not-an-email" }))
404
.send()
405
.await
406
-
.expect("Failed to send request");
407
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
408
let body: Value = res.json().await.expect("Invalid JSON");
409
-
assert_eq!(body["error"], "InvalidEmail");
410
}
···
63
}
64
65
#[tokio::test]
66
+
async fn test_request_email_update_returns_token_required() {
67
+
let client = common::client();
68
+
let base_url = common::base_url().await;
69
+
let handle = format!("emailreq-{}", uuid::Uuid::new_v4());
70
+
let email = format!("{}@example.com", handle);
71
+
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
72
+
73
+
let res = client
74
+
.post(format!(
75
+
"{}/xrpc/com.atproto.server.requestEmailUpdate",
76
+
base_url
77
+
))
78
+
.bearer_auth(&access_jwt)
79
+
.send()
80
+
.await
81
+
.expect("Failed to request email update");
82
+
assert_eq!(res.status(), StatusCode::OK);
83
+
let body: Value = res.json().await.expect("Invalid JSON");
84
+
assert_eq!(body["tokenRequired"], true);
85
+
}
86
+
87
+
#[tokio::test]
88
+
async fn test_update_email_flow_success() {
89
let client = common::client();
90
let base_url = common::base_url().await;
91
let pool = get_pool().await;
···
93
let email = format!("{}@example.com", handle);
94
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
95
let new_email = format!("new_{}@example.com", handle);
96
+
97
let res = client
98
.post(format!(
99
"{}/xrpc/com.atproto.server.requestEmailUpdate",
100
base_url
101
))
102
.bearer_auth(&access_jwt)
103
.send()
104
.await
105
.expect("Failed to request email update");
···
108
assert_eq!(body["tokenRequired"], true);
109
110
let code = get_email_update_token(&pool, &did).await;
111
+
112
let res = client
113
+
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
114
.bearer_auth(&access_jwt)
115
.json(&json!({
116
"email": new_email,
···
118
}))
119
.send()
120
.await
121
+
.expect("Failed to update email");
122
assert_eq!(res.status(), StatusCode::OK);
123
+
124
+
let user_email: Option<String> = sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
125
.fetch_one(&pool)
126
.await
127
.expect("User not found");
128
+
assert_eq!(user_email, Some(new_email));
129
}
130
131
#[tokio::test]
132
+
async fn test_update_email_requires_token_when_verified() {
133
let client = common::client();
134
let base_url = common::base_url().await;
135
+
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
136
+
let email = format!("{}@example.com", handle);
137
+
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
138
+
let new_email = format!("direct_{}@example.com", handle);
139
+
140
let res = client
141
+
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
142
+
.bearer_auth(&access_jwt)
143
+
.json(&json!({ "email": new_email }))
144
.send()
145
.await
146
+
.expect("Failed to update email");
147
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
148
let body: Value = res.json().await.expect("Invalid JSON");
149
+
assert_eq!(body["error"], "TokenRequired");
150
}
151
152
#[tokio::test]
153
+
async fn test_update_email_same_email_noop() {
154
let client = common::client();
155
let base_url = common::base_url().await;
156
+
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
157
let email = format!("{}@example.com", handle);
158
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
159
+
160
let res = client
161
+
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
162
.bearer_auth(&access_jwt)
163
+
.json(&json!({ "email": email }))
164
.send()
165
.await
166
+
.expect("Failed to update email");
167
+
assert_eq!(
168
+
res.status(),
169
+
StatusCode::OK,
170
+
"Updating to same email should succeed as no-op"
171
+
);
172
}
173
174
#[tokio::test]
175
+
async fn test_update_email_invalid_token() {
176
let client = common::client();
177
let base_url = common::base_url().await;
178
+
let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4());
179
let email = format!("{}@example.com", handle);
180
+
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
181
+
let new_email = format!("badtok_{}@example.com", handle);
182
+
183
let res = client
184
.post(format!(
185
"{}/xrpc/com.atproto.server.requestEmailUpdate",
186
base_url
187
))
188
.bearer_auth(&access_jwt)
189
.send()
190
.await
191
.expect("Failed to request email update");
192
assert_eq!(res.status(), StatusCode::OK);
193
+
194
let res = client
195
+
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
196
.bearer_auth(&access_jwt)
197
.json(&json!({
198
+
"email": new_email,
199
+
"token": "wrong-token-12345"
200
}))
201
.send()
202
.await
203
+
.expect("Failed to attempt email update");
204
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
205
let body: Value = res.json().await.expect("Invalid JSON");
206
+
assert_eq!(body["error"], "InvalidToken");
207
}
208
209
#[tokio::test]
210
+
async fn test_update_email_no_auth() {
211
let client = common::client();
212
let base_url = common::base_url().await;
213
+
214
let res = client
215
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
216
+
.json(&json!({ "email": "test@example.com" }))
217
.send()
218
.await
219
+
.expect("Failed to send request");
220
+
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
221
let body: Value = res.json().await.expect("Invalid JSON");
222
+
assert_eq!(body["error"], "AuthenticationRequired");
223
}
224
225
#[tokio::test]
226
+
async fn test_update_email_invalid_format() {
227
let client = common::client();
228
let base_url = common::base_url().await;
229
+
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
230
let email = format!("{}@example.com", handle);
231
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
232
+
233
let res = client
234
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
235
.bearer_auth(&access_jwt)
236
+
.json(&json!({ "email": "not-an-email" }))
237
.send()
238
.await
239
+
.expect("Failed to send request");
240
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
241
}
242
243
#[tokio::test]
244
+
async fn test_confirm_email_confirms_existing_email() {
245
let client = common::client();
246
let base_url = common::base_url().await;
247
+
let pool = get_pool().await;
248
+
let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4());
249
let email = format!("{}@example.com", handle);
250
+
251
let res = client
252
.post(format!(
253
+
"{}/xrpc/com.atproto.server.createAccount",
254
base_url
255
))
256
+
.json(&json!({
257
+
"handle": handle,
258
+
"email": email,
259
+
"password": "Testpass123!"
260
+
}))
261
.send()
262
.await
263
+
.expect("Failed to create account");
264
assert_eq!(res.status(), StatusCode::OK);
265
+
let body: Value = res.json().await.expect("Invalid JSON");
266
+
let did = body["did"].as_str().expect("No did").to_string();
267
+
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
268
+
269
+
let body_text: String = sqlx::query_scalar!(
270
+
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
271
+
did
272
+
)
273
+
.fetch_one(&pool)
274
+
.await
275
+
.expect("Verification email not found");
276
+
277
+
let code = body_text
278
+
.lines()
279
+
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
280
+
.map(|s| s.trim().to_string())
281
+
.unwrap_or_default();
282
+
283
let res = client
284
+
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
285
.bearer_auth(&access_jwt)
286
+
.json(&json!({
287
+
"email": email,
288
+
"token": code
289
+
}))
290
.send()
291
.await
292
+
.expect("Failed to confirm email");
293
+
assert_eq!(res.status(), StatusCode::OK);
294
+
295
+
let verified: bool = sqlx::query_scalar!(
296
+
"SELECT email_verified FROM users WHERE did = $1",
297
+
did
298
+
)
299
+
.fetch_one(&pool)
300
+
.await
301
+
.expect("User not found");
302
+
assert!(verified);
303
}
304
305
#[tokio::test]
306
+
async fn test_confirm_email_rejects_wrong_email() {
307
let client = common::client();
308
let base_url = common::base_url().await;
309
let pool = get_pool().await;
310
+
let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4());
311
let email = format!("{}@example.com", handle);
312
+
313
let res = client
314
.post(format!(
315
+
"{}/xrpc/com.atproto.server.createAccount",
316
base_url
317
))
318
+
.json(&json!({
319
+
"handle": handle,
320
+
"email": email,
321
+
"password": "Testpass123!"
322
+
}))
323
.send()
324
.await
325
+
.expect("Failed to create account");
326
assert_eq!(res.status(), StatusCode::OK);
327
+
let body: Value = res.json().await.expect("Invalid JSON");
328
+
let did = body["did"].as_str().expect("No did").to_string();
329
+
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
330
+
331
+
let body_text: String = sqlx::query_scalar!(
332
+
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
333
+
did
334
+
)
335
+
.fetch_one(&pool)
336
+
.await
337
+
.expect("Verification email not found");
338
+
339
+
let code = body_text
340
+
.lines()
341
+
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
342
+
.map(|s| s.trim().to_string())
343
+
.unwrap_or_default();
344
+
345
let res = client
346
+
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
347
.bearer_auth(&access_jwt)
348
.json(&json!({
349
+
"email": "different@example.com",
350
"token": code
351
}))
352
.send()
353
.await
354
+
.expect("Failed to confirm email");
355
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
356
+
let body: Value = res.json().await.expect("Invalid JSON");
357
+
assert_eq!(body["error"], "InvalidEmail");
358
}
359
360
#[tokio::test]
361
+
async fn test_confirm_email_invalid_token() {
362
let client = common::client();
363
let base_url = common::base_url().await;
364
+
let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4());
365
let email = format!("{}@example.com", handle);
366
+
367
let res = client
368
.post(format!(
369
+
"{}/xrpc/com.atproto.server.createAccount",
370
base_url
371
))
372
+
.json(&json!({
373
+
"handle": handle,
374
+
"email": email,
375
+
"password": "Testpass123!"
376
+
}))
377
.send()
378
.await
379
+
.expect("Failed to create account");
380
assert_eq!(res.status(), StatusCode::OK);
381
+
let body: Value = res.json().await.expect("Invalid JSON");
382
+
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
383
+
384
let res = client
385
+
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
386
.bearer_auth(&access_jwt)
387
.json(&json!({
388
+
"email": email,
389
+
"token": "wrong-token"
390
}))
391
.send()
392
.await
393
+
.expect("Failed to confirm email");
394
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
395
let body: Value = res.json().await.expect("Invalid JSON");
396
assert_eq!(body["error"], "InvalidToken");
397
}
398
399
#[tokio::test]
400
+
async fn test_unverified_account_can_update_email_without_token() {
401
let client = common::client();
402
let base_url = common::base_url().await;
403
+
let pool = get_pool().await;
404
+
let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4());
405
+
let email = format!("{}@example.com", handle);
406
+
407
let res = client
408
+
.post(format!(
409
+
"{}/xrpc/com.atproto.server.createAccount",
410
+
base_url
411
+
))
412
+
.json(&json!({
413
+
"handle": handle,
414
+
"email": email,
415
+
"password": "Testpass123!"
416
+
}))
417
.send()
418
.await
419
+
.expect("Failed to create account");
420
+
assert_eq!(res.status(), StatusCode::OK);
421
let body: Value = res.json().await.expect("Invalid JSON");
422
+
let did = body["did"].as_str().expect("No did").to_string();
423
+
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
424
+
425
+
let res = client
426
+
.post(format!(
427
+
"{}/xrpc/com.atproto.server.requestEmailUpdate",
428
+
base_url
429
+
))
430
+
.bearer_auth(&access_jwt)
431
+
.send()
432
+
.await
433
+
.expect("Failed to request email update");
434
+
assert_eq!(res.status(), StatusCode::OK);
435
+
let body: Value = res.json().await.expect("Invalid JSON");
436
+
assert_eq!(
437
+
body["tokenRequired"], false,
438
+
"Unverified account should not require token"
439
);
440
441
+
let new_email = format!("new_{}@example.com", handle);
442
let res = client
443
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
444
+
.bearer_auth(&access_jwt)
445
+
.json(&json!({ "email": new_email }))
446
.send()
447
.await
448
+
.expect("Failed to update email");
449
+
assert_eq!(
450
+
res.status(),
451
+
StatusCode::OK,
452
+
"Unverified account should be able to update email without token"
453
+
);
454
+
455
+
let user_email: Option<String> =
456
+
sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
457
+
.fetch_one(&pool)
458
+
.await
459
+
.expect("User not found");
460
+
assert_eq!(user_email, Some(new_email));
461
}
462
463
#[tokio::test]
464
+
async fn test_update_email_taken_by_another_user() {
465
let client = common::client();
466
let base_url = common::base_url().await;
467
+
let pool = get_pool().await;
468
+
469
+
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
470
+
let email1 = format!("{}@example.com", handle1);
471
+
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
472
+
473
+
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
474
+
let email2 = format!("{}@example.com", handle2);
475
+
let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await;
476
+
477
+
let res = client
478
+
.post(format!(
479
+
"{}/xrpc/com.atproto.server.requestEmailUpdate",
480
+
base_url
481
+
))
482
+
.bearer_auth(&access_jwt2)
483
+
.send()
484
+
.await
485
+
.expect("Failed to request email update");
486
+
assert_eq!(res.status(), StatusCode::OK);
487
+
488
+
let code = get_email_update_token(&pool, &did2).await;
489
+
490
let res = client
491
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
492
+
.bearer_auth(&access_jwt2)
493
+
.json(&json!({
494
+
"email": email1,
495
+
"token": code
496
+
}))
497
.send()
498
.await
499
+
.expect("Failed to update email");
500
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
501
let body: Value = res.json().await.expect("Invalid JSON");
502
+
assert_eq!(body["error"], "InvalidRequest");
503
+
assert!(body["message"]
504
+
.as_str()
505
+
.unwrap_or("")
506
+
.contains("already in use"));
507
}