mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-09 05:40:09 +00:00
Email conf. vs ref
This commit is contained in:
14
.sqlx/query-92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439.json
generated
Normal file
14
.sqlx/query-92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "92d601a6ea9ca3bcbafc228b258ede6948c18f2c824be8dcce434041d386e439"
|
||||
}
|
||||
15
.sqlx/query-ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6.json
generated
Normal file
15
.sqlx/query-ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ad9d1f4dbd7075a15733e2366db78fb42554ba52b985068318ff3af0e4ad81a6"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, email FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d6ec64a262936b8def9e5cad7d021df408b3a9b80fce5a7446647fbf276092ca"
|
||||
}
|
||||
40
.sqlx/query-e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840.json
generated
Normal file
40
.sqlx/query-e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840.json
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, handle, email, email_verified FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e1d6f474116c4eade83f39956a7ce32a175f8cdfce0d30bbb2cf155aa11bc840"
|
||||
}
|
||||
@@ -279,12 +279,10 @@ export const api = {
|
||||
|
||||
async requestEmailUpdate(
|
||||
token: string,
|
||||
email: string,
|
||||
): Promise<{ tokenRequired: boolean }> {
|
||||
return xrpc("com.atproto.server.requestEmailUpdate", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: { email },
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "Deleting...",
|
||||
"messages": {
|
||||
"emailCodeSent": "Verification code sent to your notification channel",
|
||||
"emailCodeSentToCurrent": "Verification code sent to your current email address",
|
||||
"emailUpdated": "Email updated successfully",
|
||||
"emailUpdateFailed": "Failed to update email",
|
||||
"handleUpdated": "Handle updated successfully",
|
||||
@@ -659,6 +660,7 @@
|
||||
"codeResent": "Verification code resent!",
|
||||
"codeResentDetail": "Verification code sent! Check your inbox.",
|
||||
"backToLogin": "Back to Login",
|
||||
"backToSettings": "Back to Settings",
|
||||
"verifyingAccount": "Verifying account: @{handle}",
|
||||
"startOver": "Start over with a different account",
|
||||
"noPending": "No pending verification found.",
|
||||
@@ -671,7 +673,18 @@
|
||||
"continue": "Continue",
|
||||
"identifierLabel": "Email or Identifier",
|
||||
"identifierPlaceholder": "you@example.com",
|
||||
"identifierHelp": "The email address or identifier the code was sent to"
|
||||
"identifierHelp": "The email address or identifier the code was sent to",
|
||||
"emailUpdateTitle": "Update Email Address",
|
||||
"emailUpdateSubtitle": "Enter your new email address and the verification code sent to your current email.",
|
||||
"emailUpdateRequiresAuth": "You must be signed in to update your email address.",
|
||||
"emailUpdateFailed": "Failed to update email address",
|
||||
"emailUpdateCodeHelp": "The code was sent to your current email address",
|
||||
"newEmailLabel": "New Email Address",
|
||||
"newEmailPlaceholder": "new@example.com",
|
||||
"updateEmail": "Update Email",
|
||||
"updating": "Updating...",
|
||||
"emailUpdated": "Your email has been updated successfully.",
|
||||
"emailUpdatedInfo": "You may need to verify your new email address."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "Reset Password",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "Poistetaan...",
|
||||
"messages": {
|
||||
"emailCodeSent": "Vahvistuskoodi lähetetty ilmoituskanavallesi",
|
||||
"emailCodeSentToCurrent": "Vahvistuskoodi lähetetty nykyiseen sähköpostiosoitteeseesi",
|
||||
"emailUpdated": "Sähköposti päivitetty",
|
||||
"emailUpdateFailed": "Sähköpostin päivitys epäonnistui",
|
||||
"handleUpdated": "Käyttäjänimi päivitetty",
|
||||
@@ -671,7 +672,19 @@
|
||||
"noPending": "Odottavaa vahvistusta ei löytynyt.",
|
||||
"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.",
|
||||
"createAccount": "Luo tili",
|
||||
"signIn": "Kirjaudu sisään"
|
||||
"signIn": "Kirjaudu sisään",
|
||||
"backToSettings": "Takaisin asetuksiin",
|
||||
"emailUpdateCodeHelp": "Koodi lähetettiin nykyiseen sähköpostiosoitteeseesi",
|
||||
"emailUpdateFailed": "Sähköpostiosoitteen päivitys epäonnistui",
|
||||
"emailUpdateRequiresAuth": "Sinun on kirjauduttava sisään päivittääksesi sähköpostiosoitteesi.",
|
||||
"emailUpdateSubtitle": "Syötä uusi sähköpostiosoitteesi ja nykyiseen sähköpostiisi lähetetty vahvistuskoodi.",
|
||||
"emailUpdateTitle": "Päivitä sähköpostiosoite",
|
||||
"emailUpdated": "Sähköpostiosoitteesi on päivitetty.",
|
||||
"emailUpdatedInfo": "Sinun on ehkä vahvistettava uusi sähköpostiosoitteesi.",
|
||||
"newEmailLabel": "Uusi sähköpostiosoite",
|
||||
"newEmailPlaceholder": "uusi@esimerkki.fi",
|
||||
"updateEmail": "Päivitä sähköposti",
|
||||
"updating": "Päivitetään..."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "Palauta salasana",
|
||||
@@ -754,26 +767,10 @@
|
||||
"verificationMethod": "Vahvistusmenetelmä",
|
||||
"email": "Sähköpostiosoite",
|
||||
"emailPlaceholder": "sinä@esimerkki.fi",
|
||||
"discord": "Discord",
|
||||
"discordId": "Discord-käyttäjätunnus",
|
||||
"discordIdPlaceholder": "Discord-käyttäjätunnuksesi",
|
||||
"discordIdHint": "Numeerinen Discord-käyttäjätunnuksesi (ota Kehittäjätila käyttöön löytääksesi sen)",
|
||||
"telegram": "Telegram",
|
||||
"telegramUsername": "Telegram-käyttäjänimi",
|
||||
"telegramUsernamePlaceholder": "@käyttäjänimesi",
|
||||
"signal": "Signal",
|
||||
"signalNumber": "Signal-puhelinnumero",
|
||||
"signalNumberPlaceholder": "+358401234567",
|
||||
"signalNumberHint": "Sisällytä maakoodi (esim. +358 Suomelle)",
|
||||
"inviteCode": "Kutsukoodi",
|
||||
"inviteCodePlaceholder": "Syötä kutsukoodisi",
|
||||
"inviteCodeRequired": "vaaditaan",
|
||||
"didWebDescription": "Käytä DID-identiteettiä, jota isännöidään omalla verkkotunnuksellasi.",
|
||||
"didWebToggle": "Käytä ulkoista did:web",
|
||||
"externalDid": "Sinun did:web",
|
||||
"externalDidPlaceholder": "did:web:verkkotunnuksesi.fi",
|
||||
"dnsVerificationInstructions": "Vahvistaaksesi verkkotunnuksesi, lisää tämä TXT-tietue:",
|
||||
"copyDid": "Kopioi DID",
|
||||
"createButton": "Luo tili",
|
||||
"creating": "Luodaan...",
|
||||
"alreadyHaveAccount": "Onko sinulla jo tili?",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "削除中...",
|
||||
"messages": {
|
||||
"emailCodeSent": "通知チャンネルに確認コードを送信しました",
|
||||
"emailCodeSentToCurrent": "現在のメールアドレスに確認コードを送信しました",
|
||||
"emailUpdated": "メールを更新しました",
|
||||
"emailUpdateFailed": "メールの更新に失敗しました",
|
||||
"handleUpdated": "ハンドルを更新しました",
|
||||
@@ -671,7 +672,19 @@
|
||||
"noPending": "保留中の確認が見つかりません。",
|
||||
"noPendingInfo": "最近アカウントを作成して確認が必要な場合は、新しいアカウントを作成する必要があります。すでにアカウントを確認した場合は、サインインできます。",
|
||||
"createAccount": "アカウントを作成",
|
||||
"signIn": "サインイン"
|
||||
"signIn": "サインイン",
|
||||
"backToSettings": "設定に戻る",
|
||||
"emailUpdateCodeHelp": "コードは現在のメールアドレスに送信されました",
|
||||
"emailUpdateFailed": "メールアドレスの更新に失敗しました",
|
||||
"emailUpdateRequiresAuth": "メールアドレスを更新するにはサインインが必要です。",
|
||||
"emailUpdateSubtitle": "新しいメールアドレスと、現在のメールに送信された確認コードを入力してください。",
|
||||
"emailUpdateTitle": "メールアドレスの更新",
|
||||
"emailUpdated": "メールアドレスが正常に更新されました。",
|
||||
"emailUpdatedInfo": "新しいメールアドレスの確認が必要な場合があります。",
|
||||
"newEmailLabel": "新しいメールアドレス",
|
||||
"newEmailPlaceholder": "new@example.com",
|
||||
"updateEmail": "メールを更新",
|
||||
"updating": "更新中..."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "パスワードリセット",
|
||||
@@ -754,26 +767,10 @@
|
||||
"verificationMethod": "確認方法",
|
||||
"email": "メールアドレス",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"discord": "Discord",
|
||||
"discordId": "Discord ユーザー ID",
|
||||
"discordIdPlaceholder": "Discord ユーザー ID",
|
||||
"discordIdHint": "数値の Discord ユーザー ID(開発者モードを有効にして確認)",
|
||||
"telegram": "Telegram",
|
||||
"telegramUsername": "Telegram ユーザー名",
|
||||
"telegramUsernamePlaceholder": "@yourusername",
|
||||
"signal": "Signal",
|
||||
"signalNumber": "Signal 電話番号",
|
||||
"signalNumberPlaceholder": "+81XXXXXXXXXX",
|
||||
"signalNumberHint": "国番号を含めてください(例: 日本は +81)",
|
||||
"inviteCode": "招待コード",
|
||||
"inviteCodePlaceholder": "招待コードを入力",
|
||||
"inviteCodeRequired": "必須",
|
||||
"didWebDescription": "独自ドメインでホストされる DID アイデンティティを使用します。",
|
||||
"didWebToggle": "外部 did:web を使用",
|
||||
"externalDid": "あなたの did:web",
|
||||
"externalDidPlaceholder": "did:web:yourdomain.com",
|
||||
"dnsVerificationInstructions": "ドメインを確認するには、この TXT レコードを追加してください:",
|
||||
"copyDid": "DID をコピー",
|
||||
"createButton": "アカウントを作成",
|
||||
"creating": "作成中...",
|
||||
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
|
||||
@@ -896,32 +893,15 @@
|
||||
"delegation": {
|
||||
"title": "アカウント委任",
|
||||
"controllers": "コントローラー",
|
||||
"controllersDescription": "コントローラーはあなたのアカウントの管理者として行動できます。あなたが許可した操作を実行し、あなたの代わりに投稿を作成し、リポジトリを変更できます。",
|
||||
"controlledAccounts": "管理アカウント",
|
||||
"controlledAccountsDescription": "これらはあなたがコントローラーとして追加されているアカウントです。これらのアカウントで許可されたアクションを実行できます。",
|
||||
"noControllers": "コントローラーはまだいません",
|
||||
"noControlledAccounts": "管理アカウントはありません",
|
||||
"addController": "コントローラーを追加",
|
||||
"revokeAccess": "アクセスを取り消す",
|
||||
"revokeConfirm": "このコントローラーのアクセスを取り消しますか?あなたのアカウントで操作できなくなります。",
|
||||
"handle": "ハンドル",
|
||||
"handlePlaceholder": "@user.bsky.social",
|
||||
"did": "DID",
|
||||
"didPlaceholder": "did:plc:...",
|
||||
"scopes": "権限レベル",
|
||||
"scopeOwner": "オーナー",
|
||||
"scopeOwnerDesc": "完全な管理(すべてのアクションを実行可能)",
|
||||
"scopeAdmin": "管理者",
|
||||
"scopeAdminDesc": "投稿、アプリパスワード、設定の管理",
|
||||
"scopeEditor": "編集者",
|
||||
"scopeEditorDesc": "投稿、いいね、フォローの作成・管理",
|
||||
"scopeViewer": "閲覧者",
|
||||
"scopeViewerDesc": "リポジトリと設定の読み取り専用アクセス",
|
||||
"scopeCustom": "カスタム",
|
||||
"scopeCustomDesc": "個別の権限を選択",
|
||||
"grantedAt": "許可日時",
|
||||
"expiresAt": "有効期限",
|
||||
"noExpiration": "無期限",
|
||||
"actAs": "として行動",
|
||||
"auditLog": "監査ログ",
|
||||
"auditLogTitle": "委任監査ログ",
|
||||
@@ -944,13 +924,7 @@
|
||||
"showing": "{start}~{end} / {total}件",
|
||||
"refresh": "更新",
|
||||
"failedToLoadAuditLog": "監査ログの読み込みに失敗しました",
|
||||
"addControllerTitle": "コントローラーを追加",
|
||||
"addControllerDescription": "このアカウントに対して指定した権限で操作できるユーザーを追加します。",
|
||||
"controllerIdentifier": "コントローラーのハンドルまたはDID",
|
||||
"selectScopes": "権限レベルを選択",
|
||||
"add": "追加",
|
||||
"adding": "追加中...",
|
||||
"cancel": "キャンセル",
|
||||
"accessLevel": "アクセスレベル",
|
||||
"addControllerButton": "+ コントローラーを追加",
|
||||
"auditLogDesc": "すべての委任アクティビティを表示",
|
||||
@@ -974,7 +948,8 @@
|
||||
"remove": "削除",
|
||||
"removeConfirm": "このコントローラーを削除しますか?",
|
||||
"viewAuditLog": "監査ログを表示",
|
||||
"yourAccessLevel": "あなたのアクセスレベル"
|
||||
"yourAccessLevel": "あなたのアクセスレベル",
|
||||
"accountCreated": "委任アカウントを作成しました: {handle}"
|
||||
},
|
||||
"actAs": {
|
||||
"title": "として行動",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "삭제 중...",
|
||||
"messages": {
|
||||
"emailCodeSent": "알림 채널로 인증 코드를 보냈습니다",
|
||||
"emailCodeSentToCurrent": "현재 이메일 주소로 인증 코드를 보냈습니다",
|
||||
"emailUpdated": "이메일이 업데이트되었습니다",
|
||||
"emailUpdateFailed": "이메일 업데이트에 실패했습니다",
|
||||
"handleUpdated": "핸들이 업데이트되었습니다",
|
||||
@@ -671,7 +672,19 @@
|
||||
"noPending": "보류 중인 인증이 없습니다.",
|
||||
"noPendingInfo": "최근에 계정을 만들고 인증이 필요한 경우 새 계정을 만들어야 합니다. 이미 계정을 인증한 경우 로그인할 수 있습니다.",
|
||||
"createAccount": "계정 만들기",
|
||||
"signIn": "로그인"
|
||||
"signIn": "로그인",
|
||||
"backToSettings": "설정으로 돌아가기",
|
||||
"emailUpdateCodeHelp": "코드가 현재 이메일 주소로 전송되었습니다",
|
||||
"emailUpdateFailed": "이메일 주소 업데이트 실패",
|
||||
"emailUpdateRequiresAuth": "이메일 주소를 업데이트하려면 로그인해야 합니다.",
|
||||
"emailUpdateSubtitle": "새 이메일 주소와 현재 이메일로 전송된 인증 코드를 입력하세요.",
|
||||
"emailUpdateTitle": "이메일 주소 업데이트",
|
||||
"emailUpdated": "이메일 주소가 성공적으로 업데이트되었습니다.",
|
||||
"emailUpdatedInfo": "새 이메일 주소를 인증해야 할 수 있습니다.",
|
||||
"newEmailLabel": "새 이메일 주소",
|
||||
"newEmailPlaceholder": "new@example.com",
|
||||
"updateEmail": "이메일 업데이트",
|
||||
"updating": "업데이트 중..."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "비밀번호 재설정",
|
||||
@@ -754,26 +767,10 @@
|
||||
"verificationMethod": "인증 방법",
|
||||
"email": "이메일 주소",
|
||||
"emailPlaceholder": "you@example.com",
|
||||
"discord": "Discord",
|
||||
"discordId": "Discord 사용자 ID",
|
||||
"discordIdPlaceholder": "Discord 사용자 ID",
|
||||
"discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
|
||||
"telegram": "Telegram",
|
||||
"telegramUsername": "Telegram 사용자 이름",
|
||||
"telegramUsernamePlaceholder": "@yourusername",
|
||||
"signal": "Signal",
|
||||
"signalNumber": "Signal 전화번호",
|
||||
"signalNumberPlaceholder": "+821012345678",
|
||||
"signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
|
||||
"inviteCode": "초대 코드",
|
||||
"inviteCodePlaceholder": "초대 코드 입력",
|
||||
"inviteCodeRequired": "필수",
|
||||
"didWebDescription": "자체 도메인에서 호스팅되는 DID 아이덴티티를 사용합니다.",
|
||||
"didWebToggle": "외부 did:web 사용",
|
||||
"externalDid": "귀하의 did:web",
|
||||
"externalDidPlaceholder": "did:web:yourdomain.com",
|
||||
"dnsVerificationInstructions": "도메인을 인증하려면 이 TXT 레코드를 추가하세요:",
|
||||
"copyDid": "DID 복사",
|
||||
"createButton": "계정 만들기",
|
||||
"creating": "생성 중...",
|
||||
"alreadyHaveAccount": "이미 계정이 있으신가요?",
|
||||
@@ -896,32 +893,15 @@
|
||||
"delegation": {
|
||||
"title": "계정 위임",
|
||||
"controllers": "컨트롤러",
|
||||
"controllersDescription": "컨트롤러는 귀하의 계정 관리자로서 행동할 수 있습니다. 귀하가 허용한 작업을 수행하고, 귀하를 대신하여 게시물을 생성하고, 저장소를 수정할 수 있습니다.",
|
||||
"controlledAccounts": "관리 계정",
|
||||
"controlledAccountsDescription": "귀하가 컨트롤러로 추가된 계정들입니다. 이 계정들에서 허용된 작업을 수행할 수 있습니다.",
|
||||
"noControllers": "아직 컨트롤러가 없습니다",
|
||||
"noControlledAccounts": "관리 계정이 없습니다",
|
||||
"addController": "컨트롤러 추가",
|
||||
"revokeAccess": "액세스 취소",
|
||||
"revokeConfirm": "이 컨트롤러의 액세스를 취소하시겠습니까? 귀하의 계정에서 더 이상 작업을 수행할 수 없습니다.",
|
||||
"handle": "핸들",
|
||||
"handlePlaceholder": "@user.bsky.social",
|
||||
"did": "DID",
|
||||
"didPlaceholder": "did:plc:...",
|
||||
"scopes": "권한 수준",
|
||||
"scopeOwner": "소유자",
|
||||
"scopeOwnerDesc": "전체 관리(모든 작업 수행 가능)",
|
||||
"scopeAdmin": "관리자",
|
||||
"scopeAdminDesc": "게시물, 앱 비밀번호, 설정 관리",
|
||||
"scopeEditor": "편집자",
|
||||
"scopeEditorDesc": "게시물, 좋아요, 팔로우 생성 및 관리",
|
||||
"scopeViewer": "뷰어",
|
||||
"scopeViewerDesc": "저장소 및 설정 읽기 전용 액세스",
|
||||
"scopeCustom": "사용자 정의",
|
||||
"scopeCustomDesc": "개별 권한 선택",
|
||||
"grantedAt": "허용 일시",
|
||||
"expiresAt": "만료",
|
||||
"noExpiration": "무기한",
|
||||
"actAs": "로 활동",
|
||||
"auditLog": "감사 로그",
|
||||
"auditLogTitle": "위임 감사 로그",
|
||||
@@ -944,13 +924,7 @@
|
||||
"showing": "{start}~{end} / {total}개",
|
||||
"refresh": "새로고침",
|
||||
"failedToLoadAuditLog": "감사 로그를 불러오지 못했습니다",
|
||||
"addControllerTitle": "컨트롤러 추가",
|
||||
"addControllerDescription": "이 계정에서 지정된 권한으로 작업할 수 있는 사용자를 추가합니다.",
|
||||
"controllerIdentifier": "컨트롤러 핸들 또는 DID",
|
||||
"selectScopes": "권한 수준 선택",
|
||||
"add": "추가",
|
||||
"adding": "추가 중...",
|
||||
"cancel": "취소",
|
||||
"accessLevel": "액세스 수준",
|
||||
"addControllerButton": "+ 컨트롤러 추가",
|
||||
"auditLogDesc": "모든 위임 활동 보기",
|
||||
@@ -974,7 +948,8 @@
|
||||
"remove": "제거",
|
||||
"removeConfirm": "이 컨트롤러를 제거하시겠습니까?",
|
||||
"viewAuditLog": "감사 로그 보기",
|
||||
"yourAccessLevel": "귀하의 액세스 수준"
|
||||
"yourAccessLevel": "귀하의 액세스 수준",
|
||||
"accountCreated": "위임 계정이 생성되었습니다: {handle}"
|
||||
},
|
||||
"actAs": {
|
||||
"title": "로 활동",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "Raderar...",
|
||||
"messages": {
|
||||
"emailCodeSent": "Verifieringskod skickad till din meddelandekanal",
|
||||
"emailCodeSentToCurrent": "Verifieringskod skickad till din nuvarande e-postadress",
|
||||
"emailUpdated": "E-post uppdaterad",
|
||||
"emailUpdateFailed": "Kunde inte uppdatera e-post",
|
||||
"handleUpdated": "Användarnamn uppdaterat",
|
||||
@@ -671,7 +672,19 @@
|
||||
"noPending": "Ingen väntande verifiering hittades.",
|
||||
"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.",
|
||||
"createAccount": "Skapa konto",
|
||||
"signIn": "Logga in"
|
||||
"signIn": "Logga in",
|
||||
"backToSettings": "Tillbaka till inställningar",
|
||||
"emailUpdateCodeHelp": "Koden skickades till din nuvarande e-postadress",
|
||||
"emailUpdateFailed": "Kunde inte uppdatera e-postadress",
|
||||
"emailUpdateRequiresAuth": "Du måste vara inloggad för att uppdatera din e-postadress.",
|
||||
"emailUpdateSubtitle": "Ange din nya e-postadress och verifieringskoden som skickades till din nuvarande e-post.",
|
||||
"emailUpdateTitle": "Uppdatera e-postadress",
|
||||
"emailUpdated": "Din e-postadress har uppdaterats.",
|
||||
"emailUpdatedInfo": "Du kan behöva verifiera din nya e-postadress.",
|
||||
"newEmailLabel": "Ny e-postadress",
|
||||
"newEmailPlaceholder": "ny@exempel.se",
|
||||
"updateEmail": "Uppdatera e-post",
|
||||
"updating": "Uppdaterar..."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "Återställ lösenord",
|
||||
@@ -754,26 +767,10 @@
|
||||
"verificationMethod": "Verifieringsmetod",
|
||||
"email": "E-postadress",
|
||||
"emailPlaceholder": "du@exempel.se",
|
||||
"discord": "Discord",
|
||||
"discordId": "Discord användar-ID",
|
||||
"discordIdPlaceholder": "Ditt Discord användar-ID",
|
||||
"discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
|
||||
"telegram": "Telegram",
|
||||
"telegramUsername": "Telegram-användarnamn",
|
||||
"telegramUsernamePlaceholder": "@dittanvändarnamn",
|
||||
"signal": "Signal",
|
||||
"signalNumber": "Signal-telefonnummer",
|
||||
"signalNumberPlaceholder": "+46701234567",
|
||||
"signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
|
||||
"inviteCode": "Inbjudningskod",
|
||||
"inviteCodePlaceholder": "Ange din inbjudningskod",
|
||||
"inviteCodeRequired": "krävs",
|
||||
"didWebDescription": "Använd en DID-identitet som är lagrad på din egen domän.",
|
||||
"didWebToggle": "Använd extern did:web",
|
||||
"externalDid": "Din did:web",
|
||||
"externalDidPlaceholder": "did:web:dindomän.se",
|
||||
"dnsVerificationInstructions": "För att verifiera din domän, lägg till denna TXT-post:",
|
||||
"copyDid": "Kopiera DID",
|
||||
"createButton": "Skapa konto",
|
||||
"creating": "Skapar...",
|
||||
"alreadyHaveAccount": "Har du redan ett konto?",
|
||||
@@ -896,32 +893,15 @@
|
||||
"delegation": {
|
||||
"title": "Kontodelegering",
|
||||
"controllers": "Kontrollanter",
|
||||
"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.",
|
||||
"controlledAccounts": "Kontrollerade konton",
|
||||
"controlledAccountsDescription": "Detta är konton där du har lagts till som kontrollant. Du kan utföra tillåtna åtgärder på dessa konton.",
|
||||
"noControllers": "Inga kontrollanter ännu",
|
||||
"noControlledAccounts": "Inga kontrollerade konton",
|
||||
"addController": "Lägg till kontrollant",
|
||||
"revokeAccess": "Återkalla åtkomst",
|
||||
"revokeConfirm": "Återkalla denna kontrollants åtkomst? De kommer inte längre kunna utföra åtgärder på ditt konto.",
|
||||
"handle": "Användarnamn",
|
||||
"handlePlaceholder": "@user.bsky.social",
|
||||
"did": "DID",
|
||||
"didPlaceholder": "did:plc:...",
|
||||
"scopes": "Behörighetsnivå",
|
||||
"scopeOwner": "Ägare",
|
||||
"scopeOwnerDesc": "Fullständig kontroll (kan utföra alla åtgärder)",
|
||||
"scopeAdmin": "Administratör",
|
||||
"scopeAdminDesc": "Hantera inlägg, applösenord, inställningar",
|
||||
"scopeEditor": "Redaktör",
|
||||
"scopeEditorDesc": "Skapa och hantera inlägg, gillningar, följningar",
|
||||
"scopeViewer": "Läsare",
|
||||
"scopeViewerDesc": "Endast läsåtkomst till dataförvaring och inställningar",
|
||||
"scopeCustom": "Anpassad",
|
||||
"scopeCustomDesc": "Välj individuella behörigheter",
|
||||
"grantedAt": "Beviljad",
|
||||
"expiresAt": "Upphör",
|
||||
"noExpiration": "Ingen utgång",
|
||||
"actAs": "Agera som",
|
||||
"auditLog": "Granskningslogg",
|
||||
"auditLogTitle": "Delegerings-granskningslogg",
|
||||
@@ -944,13 +924,7 @@
|
||||
"showing": "{start}–{end} av {total}",
|
||||
"refresh": "Uppdatera",
|
||||
"failedToLoadAuditLog": "Kunde inte ladda granskningsloggen",
|
||||
"addControllerTitle": "Lägg till kontrollant",
|
||||
"addControllerDescription": "Lägg till en användare som kan utföra åtgärder på detta konto med specificerade behörigheter.",
|
||||
"controllerIdentifier": "Kontrollantens användarnamn eller DID",
|
||||
"selectScopes": "Välj behörighetsnivå",
|
||||
"add": "Lägg till",
|
||||
"adding": "Lägger till...",
|
||||
"cancel": "Avbryt",
|
||||
"accessLevel": "Åtkomstnivå",
|
||||
"addControllerButton": "+ Lägg till kontrollant",
|
||||
"auditLogDesc": "Visa all delegeringsaktivitet",
|
||||
@@ -974,7 +948,8 @@
|
||||
"remove": "Ta bort",
|
||||
"removeConfirm": "Vill du ta bort denna kontrollant?",
|
||||
"viewAuditLog": "Visa granskningslogg",
|
||||
"yourAccessLevel": "Din åtkomstnivå"
|
||||
"yourAccessLevel": "Din åtkomstnivå",
|
||||
"accountCreated": "Skapade delegerat konto: {handle}"
|
||||
},
|
||||
"actAs": {
|
||||
"title": "Agera som",
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"deleting": "删除中...",
|
||||
"messages": {
|
||||
"emailCodeSent": "验证码已发送到您的通知渠道",
|
||||
"emailCodeSentToCurrent": "验证码已发送到您当前的邮箱地址",
|
||||
"emailUpdated": "邮箱更新成功",
|
||||
"emailUpdateFailed": "邮箱更新失败",
|
||||
"handleUpdated": "用户名更新成功",
|
||||
@@ -671,7 +672,19 @@
|
||||
"continue": "继续",
|
||||
"identifierLabel": "邮箱或标识符",
|
||||
"identifierPlaceholder": "you@example.com",
|
||||
"identifierHelp": "接收验证码的邮箱地址或标识符"
|
||||
"identifierHelp": "接收验证码的邮箱地址或标识符",
|
||||
"backToSettings": "返回设置",
|
||||
"emailUpdateCodeHelp": "验证码已发送到您当前的邮箱地址",
|
||||
"emailUpdateFailed": "更新邮箱地址失败",
|
||||
"emailUpdateRequiresAuth": "您需要登录才能更新邮箱地址。",
|
||||
"emailUpdateSubtitle": "输入您的新邮箱地址和发送到当前邮箱的验证码。",
|
||||
"emailUpdateTitle": "更新邮箱地址",
|
||||
"emailUpdated": "您的邮箱地址已成功更新。",
|
||||
"emailUpdatedInfo": "您可能需要验证新的邮箱地址。",
|
||||
"newEmailLabel": "新邮箱地址",
|
||||
"newEmailPlaceholder": "new@example.com",
|
||||
"updateEmail": "更新邮箱",
|
||||
"updating": "更新中..."
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "重置密码",
|
||||
@@ -821,7 +834,8 @@
|
||||
"passkeysNotSupported": "此浏览器不支持通行密钥。请使用其他浏览器或使用密码注册。",
|
||||
"passkeyCancelled": "通行密钥创建已取消",
|
||||
"passkeyFailed": "通行密钥注册失败"
|
||||
}
|
||||
},
|
||||
"didWebWarning1Detail": "您的身份将是 {did}。"
|
||||
},
|
||||
"trustedDevices": {
|
||||
"title": "受信任设备",
|
||||
@@ -879,32 +893,15 @@
|
||||
"delegation": {
|
||||
"title": "账户委托",
|
||||
"controllers": "控制者",
|
||||
"controllersDescription": "控制者可以作为您账户的管理员。他们可以执行您允许的操作,代表您发布帖子,以及修改您的数据仓库。",
|
||||
"controlledAccounts": "受控账户",
|
||||
"controlledAccountsDescription": "这些是您被添加为控制者的账户。您可以在这些账户上执行允许的操作。",
|
||||
"noControllers": "暂无控制者",
|
||||
"noControlledAccounts": "无受控账户",
|
||||
"addController": "添加控制者",
|
||||
"revokeAccess": "撤销访问",
|
||||
"revokeConfirm": "撤销此控制者的访问权限?他们将无法再在您的账户上执行操作。",
|
||||
"handle": "用户名",
|
||||
"handlePlaceholder": "@user.bsky.social",
|
||||
"did": "DID",
|
||||
"didPlaceholder": "did:plc:...",
|
||||
"scopes": "权限级别",
|
||||
"scopeOwner": "所有者",
|
||||
"scopeOwnerDesc": "完全控制(可执行所有操作)",
|
||||
"scopeAdmin": "管理员",
|
||||
"scopeAdminDesc": "管理帖子、应用专用密码、设置",
|
||||
"scopeEditor": "编辑者",
|
||||
"scopeEditorDesc": "创建和管理帖子、点赞、关注",
|
||||
"scopeViewer": "查看者",
|
||||
"scopeViewerDesc": "只读访问数据仓库和设置",
|
||||
"scopeCustom": "自定义",
|
||||
"scopeCustomDesc": "选择单独的权限",
|
||||
"grantedAt": "授权时间",
|
||||
"expiresAt": "过期时间",
|
||||
"noExpiration": "永不过期",
|
||||
"actAs": "代理操作",
|
||||
"auditLog": "审计日志",
|
||||
"auditLogTitle": "委托审计日志",
|
||||
@@ -928,13 +925,7 @@
|
||||
"showing": "{start}–{end} / 共{total}条",
|
||||
"refresh": "刷新",
|
||||
"failedToLoadAuditLog": "加载审计日志失败",
|
||||
"addControllerTitle": "添加控制者",
|
||||
"addControllerDescription": "添加一个可以在此账户上执行指定权限操作的用户。",
|
||||
"controllerIdentifier": "控制者用户名或 DID",
|
||||
"selectScopes": "选择权限级别",
|
||||
"add": "添加",
|
||||
"adding": "添加中...",
|
||||
"cancel": "取消",
|
||||
"accessLevel": "访问级别",
|
||||
"addControllerButton": "+ 添加控制者",
|
||||
"auditLogDesc": "查看所有委托活动",
|
||||
|
||||
@@ -56,21 +56,17 @@
|
||||
if (message?.text === text) message = null
|
||||
}, 5000)
|
||||
}
|
||||
async function handleRequestEmailUpdate(e: Event) {
|
||||
e.preventDefault()
|
||||
if (!auth.session || !newEmail) return
|
||||
async function handleRequestEmailUpdate() {
|
||||
if (!auth.session) return
|
||||
emailLoading = true
|
||||
message = null
|
||||
try {
|
||||
const result = await api.requestEmailUpdate(auth.session.accessJwt, newEmail)
|
||||
const result = await api.requestEmailUpdate(auth.session.accessJwt)
|
||||
emailTokenRequired = result.tokenRequired
|
||||
if (emailTokenRequired) {
|
||||
showMessage('success', $_('settings.messages.emailCodeSent'))
|
||||
showMessage('success', $_('settings.messages.emailCodeSentToCurrent'))
|
||||
} else {
|
||||
await api.updateEmail(auth.session.accessJwt, newEmail)
|
||||
await refreshSession()
|
||||
showMessage('success', $_('settings.messages.emailUpdated'))
|
||||
newEmail = ''
|
||||
emailTokenRequired = true
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage('error', e instanceof ApiError ? e.message : $_('settings.messages.emailUpdateFailed'))
|
||||
@@ -244,17 +240,6 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" disabled={emailLoading || !emailToken}>
|
||||
{emailLoading ? $_('settings.updating') : $_('settings.confirmEmailChange')}
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = '' }}>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<form onsubmit={handleRequestEmailUpdate}>
|
||||
<div class="field">
|
||||
<label for="new-email">{$_('settings.newEmail')}</label>
|
||||
<input
|
||||
@@ -266,10 +251,19 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={emailLoading || !newEmail}>
|
||||
{emailLoading ? $_('settings.requesting') : $_('settings.changeEmailButton')}
|
||||
</button>
|
||||
<div class="actions">
|
||||
<button type="submit" disabled={emailLoading || !emailToken || !newEmail}>
|
||||
{emailLoading ? $_('settings.updating') : $_('settings.confirmEmailChange')}
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = ''; newEmail = '' }}>
|
||||
{$_('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<button onclick={handleRequestEmailUpdate} disabled={emailLoading}>
|
||||
{emailLoading ? $_('settings.requesting') : $_('settings.changeEmailButton')}
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
<section>
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
channel: string
|
||||
}
|
||||
|
||||
type VerificationMode = 'signup' | 'token'
|
||||
type VerificationMode = 'signup' | 'token' | 'email-update'
|
||||
|
||||
let mode = $state<VerificationMode>('signup')
|
||||
let newEmail = $state('')
|
||||
let pendingVerification = $state<PendingVerification | null>(null)
|
||||
let verificationCode = $state('')
|
||||
let identifier = $state('')
|
||||
@@ -50,7 +51,12 @@
|
||||
onMount(async () => {
|
||||
const params = parseQueryParams()
|
||||
|
||||
if (params.token) {
|
||||
if (params.type === 'email-update') {
|
||||
mode = 'email-update'
|
||||
if (params.token) {
|
||||
verificationCode = params.token
|
||||
}
|
||||
} else if (params.token) {
|
||||
mode = 'token'
|
||||
verificationCode = params.token
|
||||
if (params.identifier) {
|
||||
@@ -134,6 +140,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEmailUpdate() {
|
||||
if (!verificationCode.trim() || !newEmail.trim()) return
|
||||
|
||||
if (!auth.session) {
|
||||
error = $_('verify.emailUpdateRequiresAuth')
|
||||
return
|
||||
}
|
||||
|
||||
submitting = true
|
||||
error = null
|
||||
|
||||
try {
|
||||
await api.updateEmail(auth.session.accessJwt, newEmail.trim(), verificationCode.trim())
|
||||
success = true
|
||||
successPurpose = 'email-update'
|
||||
successChannel = 'email'
|
||||
} catch (e: any) {
|
||||
if (e instanceof ApiError) {
|
||||
error = e.message
|
||||
} else {
|
||||
error = $_('verify.emailUpdateFailed')
|
||||
}
|
||||
} finally {
|
||||
submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResendCode() {
|
||||
if (mode === 'signup') {
|
||||
if (!pendingVerification || resendingCode) return
|
||||
@@ -198,7 +231,13 @@
|
||||
{:else if success}
|
||||
<div class="success-container">
|
||||
<h1>{$_('verify.verified')}</h1>
|
||||
{#if successPurpose === 'migration' || successPurpose === 'signup'}
|
||||
{#if successPurpose === 'email-update'}
|
||||
<p class="subtitle">{$_('verify.emailUpdated')}</p>
|
||||
<p class="info-text">{$_('verify.emailUpdatedInfo')}</p>
|
||||
<div class="actions">
|
||||
<a href="#/settings" class="btn">{$_('verify.backToSettings')}</a>
|
||||
</div>
|
||||
{:else if successPurpose === 'migration' || successPurpose === 'signup'}
|
||||
<p class="subtitle">{$_('verify.channelVerified', { values: { channel: channelLabel(successChannel || '') } })}</p>
|
||||
<p class="info-text">{$_('verify.canNowSignIn')}</p>
|
||||
<div class="actions">
|
||||
@@ -213,6 +252,58 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if mode === 'email-update'}
|
||||
<h1>{$_('verify.emailUpdateTitle')}</h1>
|
||||
<p class="subtitle">{$_('verify.emailUpdateSubtitle')}</p>
|
||||
|
||||
{#if !auth.session}
|
||||
<div class="message warning">{$_('verify.emailUpdateRequiresAuth')}</div>
|
||||
<div class="actions">
|
||||
<a href="#/login" class="btn">{$_('verify.signIn')}</a>
|
||||
</div>
|
||||
{:else}
|
||||
{#if error}
|
||||
<div class="message error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleEmailUpdate(); }}>
|
||||
<div class="field">
|
||||
<label for="new-email">{$_('verify.newEmailLabel')}</label>
|
||||
<input
|
||||
id="new-email"
|
||||
type="email"
|
||||
bind:value={newEmail}
|
||||
placeholder={$_('verify.newEmailPlaceholder')}
|
||||
disabled={submitting}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="verification-code">{$_('verify.codeLabel')}</label>
|
||||
<input
|
||||
id="verification-code"
|
||||
type="text"
|
||||
bind:value={verificationCode}
|
||||
placeholder={$_('verify.codePlaceholder')}
|
||||
disabled={submitting}
|
||||
required
|
||||
autocomplete="off"
|
||||
class="token-input"
|
||||
/>
|
||||
<p class="field-help">{$_('verify.emailUpdateCodeHelp')}</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={submitting || !verificationCode.trim() || !newEmail.trim()}>
|
||||
{submitting ? $_('verify.updating') : $_('verify.updateEmail')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="link-text">
|
||||
<a href="#/settings">{$_('verify.backToSettings')}</a>
|
||||
</p>
|
||||
{/if}
|
||||
{:else if mode === 'token'}
|
||||
<h1>{$_('verify.tokenTitle')}</h1>
|
||||
<p class="subtitle">{$_('verify.tokenSubtitle')}</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::auth::BearerAuth;
|
||||
use crate::state::{AppState, RateLimitKind};
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -10,16 +11,10 @@ use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestEmailUpdateInput {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
pub async fn request_email_update(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<RequestEmailUpdateInput>,
|
||||
auth: BearerAuth,
|
||||
) -> Response {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
@@ -37,41 +32,33 @@ pub async fn request_email_update(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let auth_user = match auth_result {
|
||||
Ok(user) => user,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let did = auth_user.did.clone();
|
||||
let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
let did = auth.0.did.clone();
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, handle, email, email_verified FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
Ok(None) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "account not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -80,59 +67,44 @@ pub async fn request_email_update(
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = user.id;
|
||||
let handle = user.handle;
|
||||
let current_email = user.email;
|
||||
let email = input.email.trim().to_lowercase();
|
||||
let current_email: String = match user.email {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "account does not have an email address"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if !crate::api::validation::is_valid_email(&email) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
|
||||
let token_required = user.email_verified;
|
||||
|
||||
if token_required {
|
||||
let code = crate::auth::verification_token::generate_channel_update_token(
|
||||
&did,
|
||||
"email_update",
|
||||
¤t_email.to_lowercase(),
|
||||
);
|
||||
let formatted_code =
|
||||
crate::auth::verification_token::format_token_for_display(&code);
|
||||
|
||||
let hostname =
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
if let Err(e) = crate::comms::enqueue_email_update_token(
|
||||
&state.db,
|
||||
user.id,
|
||||
&formatted_code,
|
||||
&hostname,
|
||||
)
|
||||
.into_response();
|
||||
.await
|
||||
{
|
||||
warn!("Failed to enqueue email update notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) {
|
||||
return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response();
|
||||
}
|
||||
|
||||
let exists = sqlx::query!(
|
||||
"SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
|
||||
email,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
if let Ok(Some(_)) = exists {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = crate::api::notification_prefs::request_channel_verification(
|
||||
&state.db,
|
||||
user_id,
|
||||
&did,
|
||||
"email",
|
||||
&email,
|
||||
Some(&handle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to request email verification: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
info!("Email update requested for user {}", user_id);
|
||||
(StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
|
||||
info!("Email update requested for user {}", user.id);
|
||||
(StatusCode::OK, Json(json!({ "tokenRequired": token_required }))).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -145,11 +117,12 @@ pub struct ConfirmEmailInput {
|
||||
pub async fn confirm_email(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: BearerAuth,
|
||||
Json(input): Json<ConfirmEmailInput>,
|
||||
) -> Response {
|
||||
let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
|
||||
if !state
|
||||
.check_rate_limit(RateLimitKind::AppPassword, &client_ip)
|
||||
.check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
|
||||
.await
|
||||
{
|
||||
warn!(ip = %client_ip, "Confirm email rate limit exceeded");
|
||||
@@ -163,41 +136,33 @@ pub async fn confirm_email(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let auth_user = match auth_result {
|
||||
Ok(user) => user,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
auth_user.is_oauth,
|
||||
auth_user.scope.as_deref(),
|
||||
auth.0.is_oauth,
|
||||
auth.0.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let did = auth_user.did;
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
let did = auth.0.did;
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, email, email_verified FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "AccountNotFound", "message": "user not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -206,14 +171,37 @@ pub async fn confirm_email(
|
||||
}
|
||||
};
|
||||
|
||||
let email = input.email.trim().to_lowercase();
|
||||
let current_email = match &user.email {
|
||||
Some(e) => e.to_lowercase(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let provided_email = input.email.trim().to_lowercase();
|
||||
if provided_email != current_email {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidEmail", "message": "invalid email"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if user.email_verified {
|
||||
return (StatusCode::OK, Json(json!({}))).into_response();
|
||||
}
|
||||
|
||||
let confirmation_code =
|
||||
crate::auth::verification_token::normalize_token_input(input.token.trim());
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
let verified = crate::auth::verification_token::verify_signup_token(
|
||||
&confirmation_code,
|
||||
"email",
|
||||
&email,
|
||||
&provided_email,
|
||||
);
|
||||
|
||||
match verified {
|
||||
@@ -245,25 +233,14 @@ pub async fn confirm_email(
|
||||
}
|
||||
|
||||
let update = sqlx::query!(
|
||||
"UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
|
||||
email,
|
||||
user_id
|
||||
"UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
|
||||
user.id
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update {
|
||||
error!("DB error finalizing email update: {:?}", e);
|
||||
if e.as_database_error()
|
||||
.map(|db_err| db_err.is_unique_violation())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
error!("DB error confirming email: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -271,7 +248,7 @@ pub async fn confirm_email(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
info!("Email updated for user {}", user_id);
|
||||
info!("Email confirmed for user {}", user.id);
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
|
||||
@@ -289,7 +266,7 @@ pub async fn update_email(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<UpdateEmailInput>,
|
||||
) -> Response {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
let bearer_token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
Some(t) => t,
|
||||
@@ -302,7 +279,7 @@ pub async fn update_email(
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await;
|
||||
let auth_user = match auth_result {
|
||||
Ok(user) => user,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
@@ -318,12 +295,23 @@ pub async fn update_email(
|
||||
}
|
||||
|
||||
let did = auth_user.did;
|
||||
let user = match sqlx::query!("SELECT id, email FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, email, email_verified FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
Ok(None) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "account not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -333,13 +321,17 @@ pub async fn update_email(
|
||||
};
|
||||
|
||||
let user_id = user.id;
|
||||
let current_email = user.email;
|
||||
let current_email = user.email.clone();
|
||||
let email_verified = user.email_verified;
|
||||
let new_email = input.email.trim().to_lowercase();
|
||||
|
||||
if !crate::api::validation::is_valid_email(&new_email) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
|
||||
Json(json!({
|
||||
"error": "InvalidRequest",
|
||||
"message": "This email address is not supported, please use a different email."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
@@ -350,48 +342,58 @@ pub async fn update_email(
|
||||
return (StatusCode::OK, Json(json!({}))).into_response();
|
||||
}
|
||||
|
||||
let confirmation_token = match &input.token {
|
||||
Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
&confirmation_token,
|
||||
"email",
|
||||
&new_email,
|
||||
);
|
||||
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did {
|
||||
if email_verified {
|
||||
let confirmation_token = match &input.token {
|
||||
Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(
|
||||
json!({"error": "InvalidToken", "message": "Token does not match account"}),
|
||||
),
|
||||
Json(json!({
|
||||
"error": "TokenRequired",
|
||||
"message": "confirmation token required"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let current_email_lower = current_email
|
||||
.as_ref()
|
||||
.map(|e| e.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
&confirmation_token,
|
||||
"email_update",
|
||||
¤t_email_lower,
|
||||
);
|
||||
|
||||
match verified {
|
||||
Ok(token_data) => {
|
||||
if token_data.did != did {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(
|
||||
json!({"error": "InvalidToken", "message": "Token does not match account"}),
|
||||
),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,13 +408,16 @@ pub async fn update_email(
|
||||
if let Ok(Some(_)) = exists {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
|
||||
Json(json!({
|
||||
"error": "InvalidRequest",
|
||||
"message": "This email address is already in use, please use a different email."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let update = sqlx::query!(
|
||||
"UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
|
||||
let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
|
||||
"UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
|
||||
new_email,
|
||||
user_id
|
||||
)
|
||||
@@ -420,14 +425,17 @@ pub async fn update_email(
|
||||
.await;
|
||||
|
||||
if let Err(e) = update {
|
||||
error!("DB error finalizing email update: {:?}", e);
|
||||
error!("DB error updating email: {:?}", e);
|
||||
if e.as_database_error()
|
||||
.map(|db_err| db_err.is_unique_violation())
|
||||
.map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
|
||||
Json(json!({
|
||||
"error": "InvalidRequest",
|
||||
"message": "This email address is already in use, please use a different email."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
@@ -438,6 +446,23 @@ pub async fn update_email(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let verification_token =
|
||||
crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = crate::comms::enqueue_signup_verification(
|
||||
&state.db,
|
||||
user_id,
|
||||
"email",
|
||||
&new_email,
|
||||
&formatted_token,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to send verification email to new address: {:?}", e);
|
||||
}
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, 'email_auth_factor', $2) ON CONFLICT (user_id, name) DO UPDATE SET value_json = $2",
|
||||
user_id,
|
||||
|
||||
@@ -10,9 +10,9 @@ pub use sender::{
|
||||
|
||||
pub use service::{
|
||||
CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion, enqueue_comms,
|
||||
enqueue_email_update, enqueue_migration_verification, enqueue_passkey_recovery,
|
||||
enqueue_password_reset, enqueue_plc_operation, enqueue_signup_verification, enqueue_welcome,
|
||||
queue_legacy_login_notification,
|
||||
enqueue_email_update, enqueue_email_update_token, enqueue_migration_verification,
|
||||
enqueue_passkey_recovery, enqueue_password_reset, enqueue_plc_operation,
|
||||
enqueue_signup_verification, enqueue_welcome, queue_legacy_login_notification,
|
||||
};
|
||||
|
||||
pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
|
||||
|
||||
@@ -380,6 +380,44 @@ pub async fn enqueue_email_update(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_email_update_token(
|
||||
db: &PgPool,
|
||||
user_id: Uuid,
|
||||
code: &str,
|
||||
hostname: &str,
|
||||
) -> Result<Uuid, sqlx::Error> {
|
||||
let prefs = get_user_comms_prefs(db, user_id).await?;
|
||||
let strings = get_strings(&prefs.locale);
|
||||
let current_email = prefs.email.clone().unwrap_or_default();
|
||||
let verify_page = format!("https://{}/#/verify?type=email-update", hostname);
|
||||
let verify_link = format!(
|
||||
"https://{}/#/verify?type=email-update&token={}",
|
||||
hostname,
|
||||
urlencoding::encode(code)
|
||||
);
|
||||
let body = format_message(
|
||||
strings.email_update_body,
|
||||
&[
|
||||
("handle", &prefs.handle),
|
||||
("code", code),
|
||||
("verify_page", &verify_page),
|
||||
("verify_link", &verify_link),
|
||||
],
|
||||
);
|
||||
let subject = format_message(strings.email_update_subject, &[("hostname", hostname)]);
|
||||
enqueue_comms(
|
||||
db,
|
||||
NewComms::email(
|
||||
user_id,
|
||||
super::types::CommsType::EmailUpdate,
|
||||
current_email,
|
||||
subject,
|
||||
body,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn enqueue_account_deletion(
|
||||
db: &PgPool,
|
||||
user_id: Uuid,
|
||||
|
||||
@@ -63,7 +63,29 @@ async fn create_verified_account(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_email_update_flow_success() {
|
||||
async fn test_request_email_update_returns_token_required() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle = format!("emailreq-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["tokenRequired"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_flow_success() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
@@ -71,13 +93,13 @@ async fn test_email_update_flow_success() {
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("new_{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
@@ -86,8 +108,9 @@ async fn test_email_update_flow_success() {
|
||||
assert_eq!(body["tokenRequired"], true);
|
||||
|
||||
let code = get_email_update_token(&pool, &did).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": new_email,
|
||||
@@ -95,120 +118,25 @@ async fn test_email_update_flow_success() {
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
.expect("Failed to update email");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
|
||||
|
||||
let user_email: Option<String> = sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
assert_eq!(user.email, Some(new_email));
|
||||
assert_eq!(user_email, Some(new_email));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_email_update_taken_email() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4());
|
||||
let email1 = format!("{}@example.com", handle1);
|
||||
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
|
||||
let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4());
|
||||
let email2 = format!("{}@example.com", handle2);
|
||||
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt2)
|
||||
.json(&json!({"email": email1}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "EmailTaken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_email_invalid_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("new_{}@example.com", handle);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": new_email,
|
||||
"token": "wrong-token"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_email_wrong_email() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("new_{}@example.com", handle);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let code = get_email_update_token(&pool, &did).await;
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": "another_random@example.com",
|
||||
"token": code
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert!(
|
||||
body["message"].as_str().unwrap().contains("mismatch") || body["error"] == "InvalidToken"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_requires_token() {
|
||||
async fn test_update_email_requires_token_when_verified() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("direct_{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
@@ -228,6 +156,7 @@ async fn test_update_email_same_email_noop() {
|
||||
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
@@ -242,76 +171,6 @@ async fn test_update_email_same_email_noop() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_requires_token_after_pending() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle = format!("emailup-token-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("pending_{}@example.com", handle);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({ "email": new_email }))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to attempt email update");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "TokenRequired");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_with_valid_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("valid_{}@example.com", handle);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let code = get_email_update_token(&pool, &did).await;
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": new_email,
|
||||
"token": code
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update email");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
assert_eq!(user.email, Some(new_email));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_invalid_token() {
|
||||
let client = common::client();
|
||||
@@ -320,17 +179,18 @@ async fn test_update_email_invalid_token() {
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
let new_email = format!("badtok_{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({"email": new_email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
@@ -346,39 +206,11 @@ async fn test_update_email_invalid_token() {
|
||||
assert_eq!(body["error"], "InvalidToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_already_taken() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
|
||||
let email1 = format!("{}@example.com", handle1);
|
||||
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
|
||||
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
|
||||
let email2 = format!("{}@example.com", handle2);
|
||||
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt2)
|
||||
.json(&json!({ "email": email1 }))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to attempt email update");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert!(
|
||||
body["error"] == "TokenRequired"
|
||||
|| body["message"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.contains("already in use")
|
||||
|| body["error"] == "InvalidRequest"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_no_auth() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.json(&json!({ "email": "test@example.com" }))
|
||||
@@ -397,6 +229,7 @@ async fn test_update_email_invalid_format() {
|
||||
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
@@ -405,6 +238,270 @@ async fn test_update_email_invalid_format() {
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_email_confirms_existing_email() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url
|
||||
))
|
||||
.json(&json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "Testpass123!"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let did = body["did"].as_str().expect("No did").to_string();
|
||||
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
|
||||
|
||||
let body_text: String = sqlx::query_scalar!(
|
||||
"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",
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Verification email not found");
|
||||
|
||||
let code = body_text
|
||||
.lines()
|
||||
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": email,
|
||||
"token": code
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let verified: bool = sqlx::query_scalar!(
|
||||
"SELECT email_verified FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
assert!(verified);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_email_rejects_wrong_email() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url
|
||||
))
|
||||
.json(&json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "Testpass123!"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let did = body["did"].as_str().expect("No did").to_string();
|
||||
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
|
||||
|
||||
let body_text: String = sqlx::query_scalar!(
|
||||
"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",
|
||||
did
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Verification email not found");
|
||||
|
||||
let code = body_text
|
||||
.lines()
|
||||
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": "different@example.com",
|
||||
"token": code
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidEmail");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_confirm_email_invalid_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url
|
||||
))
|
||||
.json(&json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "Testpass123!"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"email": email,
|
||||
"token": "wrong-token"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to confirm email");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unverified_account_can_update_email_without_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url
|
||||
))
|
||||
.json(&json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "Testpass123!"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let did = body["did"].as_str().expect("No did").to_string();
|
||||
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(
|
||||
body["tokenRequired"], false,
|
||||
"Unverified account should not require token"
|
||||
);
|
||||
|
||||
let new_email = format!("new_{}@example.com", handle);
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({ "email": new_email }))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update email");
|
||||
assert_eq!(
|
||||
res.status(),
|
||||
StatusCode::OK,
|
||||
"Unverified account should be able to update email without token"
|
||||
);
|
||||
|
||||
let user_email: Option<String> =
|
||||
sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
assert_eq!(user_email, Some(new_email));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_email_taken_by_another_user() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
|
||||
let email1 = format!("{}@example.com", handle1);
|
||||
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
|
||||
|
||||
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
|
||||
let email2 = format!("{}@example.com", handle2);
|
||||
let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await;
|
||||
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.requestEmailUpdate",
|
||||
base_url
|
||||
))
|
||||
.bearer_auth(&access_jwt2)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request email update");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let code = get_email_update_token(&pool, &did2).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
|
||||
.bearer_auth(&access_jwt2)
|
||||
.json(&json!({
|
||||
"email": email1,
|
||||
"token": code
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to update email");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
assert!(body["message"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.contains("already in use"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user