+15
.sqlx/query-3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4.json
+15
.sqlx/query-3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Uuid"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4"
15
+
}
-22
.sqlx/query-e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978.json
-22
.sqlx/query-e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "SELECT block_cid FROM user_blocks WHERE user_id = $1",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "block_cid",
9
-
"type_info": "Bytea"
10
-
}
11
-
],
12
-
"parameters": {
13
-
"Left": [
14
-
"Uuid"
15
-
]
16
-
},
17
-
"nullable": [
18
-
false
19
-
]
20
-
},
21
-
"hash": "e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978"
22
-
}
···
+2
-2
frontend/src/components/ReauthModal.svelte
+2
-2
frontend/src/components/ReauthModal.svelte
···
330
.tab.active {
331
background: var(--accent);
332
border-color: var(--accent);
333
-
color: white;
334
}
335
336
.modal-content {
···
375
width: 100%;
376
padding: 0.75rem 1.5rem;
377
background: var(--accent);
378
-
color: white;
379
border: none;
380
border-radius: 4px;
381
font-size: 1rem;
···
330
.tab.active {
331
background: var(--accent);
332
border-color: var(--accent);
333
+
color: var(--text-inverse);
334
}
335
336
.modal-content {
···
375
width: 100%;
376
padding: 0.75rem 1.5rem;
377
background: var(--accent);
378
+
color: var(--text-inverse);
379
border: none;
380
border-radius: 4px;
381
font-size: 1rem;
+8
frontend/src/lib/api.ts
+8
frontend/src/lib/api.ts
···
540
});
541
},
542
543
+
setPassword(token: AccessToken, newPassword: string): Promise<SuccessResponse> {
544
+
return xrpc("_account.setPassword", {
545
+
method: "POST",
546
+
token,
547
+
body: { newPassword },
548
+
});
549
+
},
550
+
551
getPasswordStatus(token: AccessToken): Promise<PasswordStatus> {
552
return xrpc("_account.getPasswordStatus", { token });
553
},
+8
frontend/src/locales/en.json
+8
frontend/src/locales/en.json
···
303
"confirmNewPasswordPlaceholder": "Confirm new password",
304
"changePasswordButton": "Change Password",
305
"changing": "Changing...",
306
"exportData": "Export Data",
307
"exportDataDescription": "Download your entire repository as a CAR (Content Addressable Archive) file. This includes all your posts, likes, follows, and other data.",
308
"downloadRepo": "Download Repository",
···
352
"handleUpdateFailed": "Failed to update handle",
353
"passwordChanged": "Password changed successfully",
354
"passwordChangeFailed": "Failed to change password",
355
"passwordsMismatch": "Passwords do not match",
356
"passwordsDoNotMatch": "Passwords do not match",
357
"passwordLength": "Password must be at least 8 characters",
···
509
"beforeProceedingItem3": "Ensure your recovery notification channel is up to date",
510
"addPasskeyFirst": "Add at least one passkey before you can remove your password.",
511
"passkeyOnlyHint": "You sign in using passkeys only. If you ever lose access to your passkeys, you can recover your account using the \"Lost passkey?\" link on the login page.",
512
"trustedDevices": "Trusted Devices",
513
"trustedDevicesDescription": "Manage devices that can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.",
514
"manageTrustedDevices": "Manage Trusted Devices",
···
303
"confirmNewPasswordPlaceholder": "Confirm new password",
304
"changePasswordButton": "Change Password",
305
"changing": "Changing...",
306
+
"setPassword": "Set Password",
307
+
"setPasswordDescription": "Your account is currently passkey-only. You can add a password to enable traditional login alongside your passkeys.",
308
+
"setPasswordButton": "Set Password",
309
+
"setting": "Setting...",
310
"exportData": "Export Data",
311
"exportDataDescription": "Download your entire repository as a CAR (Content Addressable Archive) file. This includes all your posts, likes, follows, and other data.",
312
"downloadRepo": "Download Repository",
···
356
"handleUpdateFailed": "Failed to update handle",
357
"passwordChanged": "Password changed successfully",
358
"passwordChangeFailed": "Failed to change password",
359
+
"passwordSet": "Password set successfully",
360
+
"passwordSetFailed": "Failed to set password",
361
"passwordsMismatch": "Passwords do not match",
362
"passwordsDoNotMatch": "Passwords do not match",
363
"passwordLength": "Password must be at least 8 characters",
···
515
"beforeProceedingItem3": "Ensure your recovery notification channel is up to date",
516
"addPasskeyFirst": "Add at least one passkey before you can remove your password.",
517
"passkeyOnlyHint": "You sign in using passkeys only. If you ever lose access to your passkeys, you can recover your account using the \"Lost passkey?\" link on the login page.",
518
+
"addPasswordHint": "Want to add a password? Go to Settings to set one up.",
519
+
"goToSettings": "Go to Settings",
520
"trustedDevices": "Trusted Devices",
521
"trustedDevicesDescription": "Manage devices that can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.",
522
"manageTrustedDevices": "Manage Trusted Devices",
+8
frontend/src/locales/fi.json
+8
frontend/src/locales/fi.json
···
303
"confirmNewPasswordPlaceholder": "Vahvista uusi salasana",
304
"changePasswordButton": "Vaihda salasana",
305
"changing": "Vaihdetaan...",
306
"exportData": "Vie tiedot",
307
"exportDataDescription": "Lataa koko tietovarastosi CAR-tiedostona (Content Addressable Archive). Tämä sisältää kaikki julkaisusi, tykkäyksesi, seuraamisesi ja muut tiedot.",
308
"downloadRepo": "Lataa tietovarasto",
···
352
"handleUpdateFailed": "Käyttäjänimen päivitys epäonnistui",
353
"passwordChanged": "Salasana vaihdettu",
354
"passwordChangeFailed": "Salasanan vaihto epäonnistui",
355
"passwordsMismatch": "Salasanat eivät täsmää",
356
"passwordsDoNotMatch": "Salasanat eivät täsmää",
357
"passwordLength": "Salasanan on oltava vähintään 8 merkkiä",
···
509
"beforeProceedingItem3": "Varmista, että palautusilmoituskanavasi on ajan tasalla",
510
"addPasskeyFirst": "Lisää vähintään yksi pääsyavain ennen kuin voit poistaa salasanasi.",
511
"passkeyOnlyHint": "Kirjaudut sisään vain pääsyavaimilla. Jos menetät pääsyn pääsyavaimeesi, voit palauttaa tilisi käyttämällä \"Kadotitko pääsyavaimen?\" -linkkiä kirjautumissivulla.",
512
"trustedDevices": "Luotetut laitteet",
513
"trustedDevicesDescription": "Hallitse laitteita, jotka voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.",
514
"manageTrustedDevices": "Hallitse luotettuja laitteita",
···
303
"confirmNewPasswordPlaceholder": "Vahvista uusi salasana",
304
"changePasswordButton": "Vaihda salasana",
305
"changing": "Vaihdetaan...",
306
+
"setPassword": "Aseta salasana",
307
+
"setPasswordDescription": "Tilisi on tällä hetkellä vain pääsyavain-tili. Voit lisätä salasanan ottaaksesi käyttöön perinteisen kirjautumisen pääsyavainten rinnalla.",
308
+
"setPasswordButton": "Aseta salasana",
309
+
"setting": "Asetetaan...",
310
"exportData": "Vie tiedot",
311
"exportDataDescription": "Lataa koko tietovarastosi CAR-tiedostona (Content Addressable Archive). Tämä sisältää kaikki julkaisusi, tykkäyksesi, seuraamisesi ja muut tiedot.",
312
"downloadRepo": "Lataa tietovarasto",
···
356
"handleUpdateFailed": "Käyttäjänimen päivitys epäonnistui",
357
"passwordChanged": "Salasana vaihdettu",
358
"passwordChangeFailed": "Salasanan vaihto epäonnistui",
359
+
"passwordSet": "Salasana asetettu onnistuneesti",
360
+
"passwordSetFailed": "Salasanan asettaminen epäonnistui",
361
"passwordsMismatch": "Salasanat eivät täsmää",
362
"passwordsDoNotMatch": "Salasanat eivät täsmää",
363
"passwordLength": "Salasanan on oltava vähintään 8 merkkiä",
···
515
"beforeProceedingItem3": "Varmista, että palautusilmoituskanavasi on ajan tasalla",
516
"addPasskeyFirst": "Lisää vähintään yksi pääsyavain ennen kuin voit poistaa salasanasi.",
517
"passkeyOnlyHint": "Kirjaudut sisään vain pääsyavaimilla. Jos menetät pääsyn pääsyavaimeesi, voit palauttaa tilisi käyttämällä \"Kadotitko pääsyavaimen?\" -linkkiä kirjautumissivulla.",
518
+
"addPasswordHint": "Haluatko lisätä salasanan? Siirry Asetuksiin määrittääksesi sellaisen.",
519
+
"goToSettings": "Siirry asetuksiin",
520
"trustedDevices": "Luotetut laitteet",
521
"trustedDevicesDescription": "Hallitse laitteita, jotka voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.",
522
"manageTrustedDevices": "Hallitse luotettuja laitteita",
+8
frontend/src/locales/ja.json
+8
frontend/src/locales/ja.json
···
296
"confirmNewPasswordPlaceholder": "新しいパスワードを再入力",
297
"changePasswordButton": "パスワードを変更",
298
"changing": "変更中...",
299
"exportData": "データエクスポート",
300
"exportDataDescription": "リポジトリ全体を CAR(Content Addressable Archive)ファイルとしてダウンロードします。投稿、いいね、フォローなどすべてのデータが含まれます。",
301
"downloadRepo": "リポジトリをダウンロード",
···
345
"handleUpdateFailed": "ハンドルの更新に失敗しました",
346
"passwordChanged": "パスワードを変更しました",
347
"passwordChangeFailed": "パスワードの変更に失敗しました",
348
"passwordsMismatch": "パスワードが一致しません",
349
"passwordsDoNotMatch": "パスワードが一致しません",
350
"passwordLength": "パスワードは8文字以上である必要があります",
···
502
"beforeProceedingItem3": "復旧用の通知チャンネルが最新であることを確認",
503
"addPasskeyFirst": "パスワードを削除する前に、少なくとも1つのパスキーを追加してください。",
504
"passkeyOnlyHint": "パスキーのみでサインインしています。パスキーにアクセスできなくなった場合、ログインページの「パスキーを紛失しましたか?」リンクからアカウントを復旧できます。",
505
"trustedDevices": "信頼済みデバイス",
506
"trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイスを管理します。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。",
507
"manageTrustedDevices": "信頼済みデバイスを管理",
···
296
"confirmNewPasswordPlaceholder": "新しいパスワードを再入力",
297
"changePasswordButton": "パスワードを変更",
298
"changing": "変更中...",
299
+
"setPassword": "パスワードを設定",
300
+
"setPasswordDescription": "現在、あなたのアカウントはパスキーのみです。パスワードを追加すると、パスキーと併せて従来のログインも使用できます。",
301
+
"setPasswordButton": "パスワードを設定",
302
+
"setting": "設定中...",
303
"exportData": "データエクスポート",
304
"exportDataDescription": "リポジトリ全体を CAR(Content Addressable Archive)ファイルとしてダウンロードします。投稿、いいね、フォローなどすべてのデータが含まれます。",
305
"downloadRepo": "リポジトリをダウンロード",
···
349
"handleUpdateFailed": "ハンドルの更新に失敗しました",
350
"passwordChanged": "パスワードを変更しました",
351
"passwordChangeFailed": "パスワードの変更に失敗しました",
352
+
"passwordSet": "パスワードを設定しました",
353
+
"passwordSetFailed": "パスワードの設定に失敗しました",
354
"passwordsMismatch": "パスワードが一致しません",
355
"passwordsDoNotMatch": "パスワードが一致しません",
356
"passwordLength": "パスワードは8文字以上である必要があります",
···
508
"beforeProceedingItem3": "復旧用の通知チャンネルが最新であることを確認",
509
"addPasskeyFirst": "パスワードを削除する前に、少なくとも1つのパスキーを追加してください。",
510
"passkeyOnlyHint": "パスキーのみでサインインしています。パスキーにアクセスできなくなった場合、ログインページの「パスキーを紛失しましたか?」リンクからアカウントを復旧できます。",
511
+
"addPasswordHint": "パスワードを追加しますか?設定で追加できます。",
512
+
"goToSettings": "設定へ移動",
513
"trustedDevices": "信頼済みデバイス",
514
"trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイスを管理します。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。",
515
"manageTrustedDevices": "信頼済みデバイスを管理",
+8
frontend/src/locales/ko.json
+8
frontend/src/locales/ko.json
···
296
"confirmNewPasswordPlaceholder": "새 비밀번호 재입력",
297
"changePasswordButton": "비밀번호 변경",
298
"changing": "변경 중...",
299
"exportData": "데이터 내보내기",
300
"exportDataDescription": "전체 저장소를 CAR (Content Addressable Archive) 파일로 다운로드합니다. 모든 게시물, 좋아요, 팔로우 및 기타 데이터가 포함됩니다.",
301
"downloadRepo": "저장소 다운로드",
···
345
"handleUpdateFailed": "핸들 업데이트에 실패했습니다",
346
"passwordChanged": "비밀번호가 변경되었습니다",
347
"passwordChangeFailed": "비밀번호 변경에 실패했습니다",
348
"passwordsMismatch": "비밀번호가 일치하지 않습니다",
349
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다",
350
"passwordLength": "비밀번호는 8자 이상이어야 합니다",
···
502
"beforeProceedingItem3": "복구 알림 채널이 최신인지 확인",
503
"addPasskeyFirst": "비밀번호를 제거하려면 먼저 최소 하나의 패스키를 추가하세요.",
504
"passkeyOnlyHint": "패스키로만 로그인합니다. 패스키에 액세스할 수 없게 되면 로그인 페이지의 '패스키를 분실하셨나요?' 링크를 사용하여 계정을 복구할 수 있습니다.",
505
"trustedDevices": "신뢰할 수 있는 기기",
506
"trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기를 관리합니다. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.",
507
"manageTrustedDevices": "신뢰할 수 있는 기기 관리",
···
296
"confirmNewPasswordPlaceholder": "새 비밀번호 재입력",
297
"changePasswordButton": "비밀번호 변경",
298
"changing": "변경 중...",
299
+
"setPassword": "비밀번호 설정",
300
+
"setPasswordDescription": "현재 계정은 패스키 전용입니다. 비밀번호를 추가하면 패스키와 함께 기존 로그인 방식도 사용할 수 있습니다.",
301
+
"setPasswordButton": "비밀번호 설정",
302
+
"setting": "설정 중...",
303
"exportData": "데이터 내보내기",
304
"exportDataDescription": "전체 저장소를 CAR (Content Addressable Archive) 파일로 다운로드합니다. 모든 게시물, 좋아요, 팔로우 및 기타 데이터가 포함됩니다.",
305
"downloadRepo": "저장소 다운로드",
···
349
"handleUpdateFailed": "핸들 업데이트에 실패했습니다",
350
"passwordChanged": "비밀번호가 변경되었습니다",
351
"passwordChangeFailed": "비밀번호 변경에 실패했습니다",
352
+
"passwordSet": "비밀번호가 설정되었습니다",
353
+
"passwordSetFailed": "비밀번호 설정에 실패했습니다",
354
"passwordsMismatch": "비밀번호가 일치하지 않습니다",
355
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다",
356
"passwordLength": "비밀번호는 8자 이상이어야 합니다",
···
508
"beforeProceedingItem3": "복구 알림 채널이 최신인지 확인",
509
"addPasskeyFirst": "비밀번호를 제거하려면 먼저 최소 하나의 패스키를 추가하세요.",
510
"passkeyOnlyHint": "패스키로만 로그인합니다. 패스키에 액세스할 수 없게 되면 로그인 페이지의 '패스키를 분실하셨나요?' 링크를 사용하여 계정을 복구할 수 있습니다.",
511
+
"addPasswordHint": "비밀번호를 추가하시겠습니까? 설정에서 설정하세요.",
512
+
"goToSettings": "설정으로 이동",
513
"trustedDevices": "신뢰할 수 있는 기기",
514
"trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기를 관리합니다. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.",
515
"manageTrustedDevices": "신뢰할 수 있는 기기 관리",
+8
frontend/src/locales/sv.json
+8
frontend/src/locales/sv.json
···
296
"confirmNewPasswordPlaceholder": "Bekräfta nytt lösenord",
297
"changePasswordButton": "Ändra lösenord",
298
"changing": "Ändrar...",
299
"exportData": "Exportera data",
300
"exportDataDescription": "Ladda ner hela ditt arkiv som en CAR-fil (Content Addressable Archive). Detta inkluderar alla dina inlägg, gillanden, följningar och annan data.",
301
"downloadRepo": "Ladda ner arkiv",
···
345
"handleUpdateFailed": "Kunde inte uppdatera användarnamn",
346
"passwordChanged": "Lösenord ändrat",
347
"passwordChangeFailed": "Kunde inte ändra lösenord",
348
"passwordsMismatch": "Lösenorden matchar inte",
349
"passwordsDoNotMatch": "Lösenorden matchar inte",
350
"passwordLength": "Lösenordet måste vara minst 8 tecken",
···
502
"beforeProceedingItem3": "Se till att din meddelandekanal för återställning är uppdaterad",
503
"addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.",
504
"passkeyOnlyHint": "Du loggar in med endast nycklar. Om du förlorar tillgång till dina nycklar kan du återställa ditt konto med länken \"Tappat bort nyckeln?\" på inloggningssidan.",
505
"trustedDevices": "Betrodda enheter",
506
"trustedDevicesDescription": "Hantera enheter som kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.",
507
"manageTrustedDevices": "Hantera betrodda enheter",
···
296
"confirmNewPasswordPlaceholder": "Bekräfta nytt lösenord",
297
"changePasswordButton": "Ändra lösenord",
298
"changing": "Ändrar...",
299
+
"setPassword": "Ange lösenord",
300
+
"setPasswordDescription": "Ditt konto är för närvarande endast passnycklar. Du kan lägga till ett lösenord för att aktivera traditionell inloggning tillsammans med dina passnycklar.",
301
+
"setPasswordButton": "Ange lösenord",
302
+
"setting": "Anger...",
303
"exportData": "Exportera data",
304
"exportDataDescription": "Ladda ner hela ditt arkiv som en CAR-fil (Content Addressable Archive). Detta inkluderar alla dina inlägg, gillanden, följningar och annan data.",
305
"downloadRepo": "Ladda ner arkiv",
···
349
"handleUpdateFailed": "Kunde inte uppdatera användarnamn",
350
"passwordChanged": "Lösenord ändrat",
351
"passwordChangeFailed": "Kunde inte ändra lösenord",
352
+
"passwordSet": "Lösenord har angetts",
353
+
"passwordSetFailed": "Kunde inte ange lösenord",
354
"passwordsMismatch": "Lösenorden matchar inte",
355
"passwordsDoNotMatch": "Lösenorden matchar inte",
356
"passwordLength": "Lösenordet måste vara minst 8 tecken",
···
508
"beforeProceedingItem3": "Se till att din meddelandekanal för återställning är uppdaterad",
509
"addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.",
510
"passkeyOnlyHint": "Du loggar in med endast nycklar. Om du förlorar tillgång till dina nycklar kan du återställa ditt konto med länken \"Tappat bort nyckeln?\" på inloggningssidan.",
511
+
"addPasswordHint": "Vill du lägga till ett lösenord? Gå till Inställningar för att ställa in ett.",
512
+
"goToSettings": "Gå till inställningar",
513
"trustedDevices": "Betrodda enheter",
514
"trustedDevicesDescription": "Hantera enheter som kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.",
515
"manageTrustedDevices": "Hantera betrodda enheter",
+8
frontend/src/locales/zh.json
+8
frontend/src/locales/zh.json
···
296
"confirmNewPasswordPlaceholder": "再次输入新密码",
297
"changePasswordButton": "更改密码",
298
"changing": "更改中...",
299
"exportData": "导出数据",
300
"exportDataDescription": "将您的所有数据下载为 CAR 文件。包括您的所有帖子、点赞、关注等数据。",
301
"downloadRepo": "下载数据",
···
345
"handleUpdateFailed": "用户名更新失败",
346
"passwordChanged": "密码更改成功",
347
"passwordChangeFailed": "密码更改失败",
348
"passwordsMismatch": "两次输入的密码不一致",
349
"passwordsDoNotMatch": "两次输入的密码不一致",
350
"passwordLength": "密码至少需要8位字符",
···
502
"beforeProceedingItem3": "确保您的恢复通知渠道是最新的",
503
"addPasskeyFirst": "请先添加至少一个通行密钥才能移除密码。",
504
"passkeyOnlyHint": "您使用通行密钥登录。如果您丢失了通行密钥,可以使用登录页面上的「丢失通行密钥?」链接恢复账户。",
505
"trustedDevices": "受信任设备",
506
"trustedDevicesDescription": "管理可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。",
507
"manageTrustedDevices": "管理受信任设备",
···
296
"confirmNewPasswordPlaceholder": "再次输入新密码",
297
"changePasswordButton": "更改密码",
298
"changing": "更改中...",
299
+
"setPassword": "设置密码",
300
+
"setPasswordDescription": "您的账户当前仅使用通行密钥。您可以添加密码以启用传统登录方式与通行密钥并用。",
301
+
"setPasswordButton": "设置密码",
302
+
"setting": "设置中...",
303
"exportData": "导出数据",
304
"exportDataDescription": "将您的所有数据下载为 CAR 文件。包括您的所有帖子、点赞、关注等数据。",
305
"downloadRepo": "下载数据",
···
349
"handleUpdateFailed": "用户名更新失败",
350
"passwordChanged": "密码更改成功",
351
"passwordChangeFailed": "密码更改失败",
352
+
"passwordSet": "密码设置成功",
353
+
"passwordSetFailed": "密码设置失败",
354
"passwordsMismatch": "两次输入的密码不一致",
355
"passwordsDoNotMatch": "两次输入的密码不一致",
356
"passwordLength": "密码至少需要8位字符",
···
508
"beforeProceedingItem3": "确保您的恢复通知渠道是最新的",
509
"addPasskeyFirst": "请先添加至少一个通行密钥才能移除密码。",
510
"passkeyOnlyHint": "您使用通行密钥登录。如果您丢失了通行密钥,可以使用登录页面上的「丢失通行密钥?」链接恢复账户。",
511
+
"addPasswordHint": "想要添加密码?前往设置进行设置。",
512
+
"goToSettings": "前往设置",
513
"trustedDevices": "受信任设备",
514
"trustedDevicesDescription": "管理可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。",
515
"manageTrustedDevices": "管理受信任设备",
+6
frontend/src/routes/Security.svelte
+6
frontend/src/routes/Security.svelte
+160
-42
frontend/src/routes/Settings.svelte
+160
-42
frontend/src/routes/Settings.svelte
···
8
import { unsafeAsHandle } from '../lib/types/branded'
9
import type { Session } from '../lib/types/api'
10
import { toast } from '../lib/toast.svelte'
11
12
const auth = $derived(getAuthState())
13
const supportedLocales = getSupportedLocales()
···
63
let newPassword = $state('')
64
let confirmNewPassword = $state('')
65
let showBYOHandle = $state(false)
66
67
$effect(() => {
68
if (!loading && !session) {
69
navigate(routes.login)
70
}
71
})
72
73
async function handleRequestEmailUpdate() {
74
if (!session) return
···
374
passwordLoading = false
375
}
376
}
377
</script>
378
<div class="page">
379
<header>
···
522
</form>
523
{/if}
524
</section>
525
-
<section>
526
-
<h2>{$_('settings.changePassword')}</h2>
527
-
<form onsubmit={handleChangePassword}>
528
-
<div class="field">
529
-
<label for="current-password">{$_('settings.currentPassword')}</label>
530
-
<input
531
-
id="current-password"
532
-
type="password"
533
-
bind:value={currentPassword}
534
-
placeholder={$_('settings.currentPasswordPlaceholder')}
535
-
disabled={passwordLoading}
536
-
required
537
-
/>
538
-
</div>
539
-
<div class="field">
540
-
<label for="new-password">{$_('settings.newPassword')}</label>
541
-
<input
542
-
id="new-password"
543
-
type="password"
544
-
bind:value={newPassword}
545
-
placeholder={$_('settings.newPasswordPlaceholder')}
546
-
disabled={passwordLoading}
547
-
required
548
-
minlength="8"
549
-
/>
550
-
</div>
551
-
<div class="field">
552
-
<label for="confirm-new-password">{$_('settings.confirmNewPassword')}</label>
553
-
<input
554
-
id="confirm-new-password"
555
-
type="password"
556
-
bind:value={confirmNewPassword}
557
-
placeholder={$_('settings.confirmNewPasswordPlaceholder')}
558
-
disabled={passwordLoading}
559
-
required
560
-
/>
561
-
</div>
562
-
<button type="submit" disabled={passwordLoading || !currentPassword || !newPassword || !confirmNewPassword}>
563
-
{passwordLoading ? $_('settings.changing') : $_('settings.changePasswordButton')}
564
-
</button>
565
-
</form>
566
-
</section>
567
<section>
568
<h2>{$_('settings.exportData')}</h2>
569
<p class="description">{$_('settings.exportDataDescription')}</p>
···
681
{/if}
682
</section>
683
</div>
684
<style>
685
.page {
686
max-width: var(--width-lg);
···
8
import { unsafeAsHandle } from '../lib/types/branded'
9
import type { Session } from '../lib/types/api'
10
import { toast } from '../lib/toast.svelte'
11
+
import ReauthModal from '../components/ReauthModal.svelte'
12
13
const auth = $derived(getAuthState())
14
const supportedLocales = getSupportedLocales()
···
64
let newPassword = $state('')
65
let confirmNewPassword = $state('')
66
let showBYOHandle = $state(false)
67
+
let hasPassword = $state(true)
68
+
let passwordStatusLoading = $state(true)
69
+
let setPasswordLoading = $state(false)
70
+
let showReauthModal = $state(false)
71
+
let reauthMethods = $state<string[]>(['passkey'])
72
+
let pendingAction = $state<(() => Promise<void>) | null>(null)
73
74
$effect(() => {
75
if (!loading && !session) {
76
navigate(routes.login)
77
}
78
})
79
+
80
+
$effect(() => {
81
+
if (session) {
82
+
loadPasswordStatus()
83
+
}
84
+
})
85
+
86
+
async function loadPasswordStatus() {
87
+
if (!session) return
88
+
passwordStatusLoading = true
89
+
try {
90
+
const status = await api.getPasswordStatus(session.accessJwt)
91
+
hasPassword = status.hasPassword
92
+
} catch {
93
+
hasPassword = true
94
+
} finally {
95
+
passwordStatusLoading = false
96
+
}
97
+
}
98
99
async function handleRequestEmailUpdate() {
100
if (!session) return
···
400
passwordLoading = false
401
}
402
}
403
+
404
+
async function handleSetPassword(e: Event) {
405
+
e.preventDefault()
406
+
if (!session || !newPassword || !confirmNewPassword) return
407
+
if (newPassword !== confirmNewPassword) {
408
+
toast.error($_('settings.messages.passwordsDoNotMatch'))
409
+
return
410
+
}
411
+
if (newPassword.length < 8) {
412
+
toast.error($_('settings.messages.passwordTooShort'))
413
+
return
414
+
}
415
+
setPasswordLoading = true
416
+
try {
417
+
await api.setPassword(session.accessJwt, newPassword)
418
+
toast.success($_('settings.messages.passwordSet'))
419
+
hasPassword = true
420
+
newPassword = ''
421
+
confirmNewPassword = ''
422
+
} catch (e) {
423
+
if (e instanceof ApiError) {
424
+
if (e.error === 'ReauthRequired') {
425
+
reauthMethods = e.reauthMethods || ['passkey']
426
+
pendingAction = () => handleSetPassword(new Event('submit'))
427
+
showReauthModal = true
428
+
} else {
429
+
toast.error(e.message)
430
+
}
431
+
} else {
432
+
toast.error($_('settings.messages.passwordSetFailed'))
433
+
}
434
+
} finally {
435
+
setPasswordLoading = false
436
+
}
437
+
}
438
+
439
+
function handleReauthSuccess() {
440
+
if (pendingAction) {
441
+
pendingAction()
442
+
pendingAction = null
443
+
}
444
+
}
445
+
446
+
function handleReauthCancel() {
447
+
pendingAction = null
448
+
}
449
</script>
450
<div class="page">
451
<header>
···
594
</form>
595
{/if}
596
</section>
597
+
{#if !passwordStatusLoading}
598
+
{#if hasPassword}
599
+
<section>
600
+
<h2>{$_('settings.changePassword')}</h2>
601
+
<form onsubmit={handleChangePassword}>
602
+
<div class="field">
603
+
<label for="current-password">{$_('settings.currentPassword')}</label>
604
+
<input
605
+
id="current-password"
606
+
type="password"
607
+
bind:value={currentPassword}
608
+
placeholder={$_('settings.currentPasswordPlaceholder')}
609
+
disabled={passwordLoading}
610
+
required
611
+
/>
612
+
</div>
613
+
<div class="field">
614
+
<label for="new-password">{$_('settings.newPassword')}</label>
615
+
<input
616
+
id="new-password"
617
+
type="password"
618
+
bind:value={newPassword}
619
+
placeholder={$_('settings.newPasswordPlaceholder')}
620
+
disabled={passwordLoading}
621
+
required
622
+
minlength="8"
623
+
/>
624
+
</div>
625
+
<div class="field">
626
+
<label for="confirm-new-password">{$_('settings.confirmNewPassword')}</label>
627
+
<input
628
+
id="confirm-new-password"
629
+
type="password"
630
+
bind:value={confirmNewPassword}
631
+
placeholder={$_('settings.confirmNewPasswordPlaceholder')}
632
+
disabled={passwordLoading}
633
+
required
634
+
/>
635
+
</div>
636
+
<button type="submit" disabled={passwordLoading || !currentPassword || !newPassword || !confirmNewPassword}>
637
+
{passwordLoading ? $_('settings.changing') : $_('settings.changePasswordButton')}
638
+
</button>
639
+
</form>
640
+
</section>
641
+
{:else}
642
+
<section>
643
+
<h2>{$_('settings.setPassword')}</h2>
644
+
<p class="description">{$_('settings.setPasswordDescription')}</p>
645
+
<form onsubmit={handleSetPassword}>
646
+
<div class="field">
647
+
<label for="set-new-password">{$_('settings.newPassword')}</label>
648
+
<input
649
+
id="set-new-password"
650
+
type="password"
651
+
bind:value={newPassword}
652
+
placeholder={$_('settings.newPasswordPlaceholder')}
653
+
disabled={setPasswordLoading}
654
+
required
655
+
minlength="8"
656
+
/>
657
+
</div>
658
+
<div class="field">
659
+
<label for="set-confirm-password">{$_('settings.confirmNewPassword')}</label>
660
+
<input
661
+
id="set-confirm-password"
662
+
type="password"
663
+
bind:value={confirmNewPassword}
664
+
placeholder={$_('settings.confirmNewPasswordPlaceholder')}
665
+
disabled={setPasswordLoading}
666
+
required
667
+
/>
668
+
</div>
669
+
<button type="submit" disabled={setPasswordLoading || !newPassword || !confirmNewPassword}>
670
+
{setPasswordLoading ? $_('settings.setting') : $_('settings.setPasswordButton')}
671
+
</button>
672
+
</form>
673
+
</section>
674
+
{/if}
675
+
{/if}
676
<section>
677
<h2>{$_('settings.exportData')}</h2>
678
<p class="description">{$_('settings.exportDataDescription')}</p>
···
790
{/if}
791
</section>
792
</div>
793
+
794
+
{#if showReauthModal && session}
795
+
<ReauthModal
796
+
bind:show={showReauthModal}
797
+
availableMethods={reauthMethods}
798
+
onSuccess={handleReauthSuccess}
799
+
onCancel={handleReauthCancel}
800
+
/>
801
+
{/if}
802
<style>
803
.page {
804
max-width: var(--width-lg);
+1
-1
src/api/error.rs
+1
-1
src/api/error.rs
+10
-13
src/api/proxy.rs
+10
-13
src/api/proxy.rs
···
268
}
269
Err(e) => {
270
warn!("Token validation failed: {:?}", e);
271
-
if matches!(e, crate::auth::TokenValidationError::TokenExpired) {
272
-
let is_dpop = extracted.is_dpop;
273
-
let scheme = if is_dpop { "DPoP" } else { "Bearer" };
274
-
let www_auth = format!(
275
-
"{} error=\"invalid_token\", error_description=\"Token has expired\"",
276
-
scheme
277
-
);
278
let mut response =
279
ApiError::ExpiredToken(Some("Token has expired".into())).into_response();
280
response
281
.headers_mut()
282
.insert("WWW-Authenticate", www_auth.parse().unwrap());
283
-
if is_dpop {
284
-
let nonce = crate::oauth::verify::generate_dpop_nonce();
285
-
response
286
-
.headers_mut()
287
-
.insert("DPoP-Nonce", nonce.parse().unwrap());
288
-
}
289
return response;
290
}
291
}
···
268
}
269
Err(e) => {
270
warn!("Token validation failed: {:?}", e);
271
+
if matches!(e, crate::auth::TokenValidationError::TokenExpired)
272
+
&& extracted.is_dpop
273
+
{
274
+
let www_auth =
275
+
"DPoP error=\"invalid_token\", error_description=\"Token has expired\"";
276
let mut response =
277
ApiError::ExpiredToken(Some("Token has expired".into())).into_response();
278
+
*response.status_mut() = axum::http::StatusCode::UNAUTHORIZED;
279
response
280
.headers_mut()
281
.insert("WWW-Authenticate", www_auth.parse().unwrap());
282
+
let nonce = crate::oauth::verify::generate_dpop_nonce();
283
+
response
284
+
.headers_mut()
285
+
.insert("DPoP-Nonce", nonce.parse().unwrap());
286
return response;
287
}
288
}
+3
src/api/repo/record/batch.rs
+3
src/api/repo/record/batch.rs
···
444
.await
445
{
446
Ok(res) => res,
447
+
Err(e) if e.contains("ConcurrentModification") => {
448
+
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
449
+
}
450
Err(e) => {
451
error!("Commit failed: {}", e);
452
return ApiError::InternalError(Some("Failed to commit changes".into()))
+3
src/api/repo/record/delete.rs
+3
src/api/repo/record/delete.rs
+12
-10
src/api/repo/record/write.rs
+12
-10
src/api/repo/record/write.rs
···
83
.map_err(|e| {
84
tracing::warn!(error = ?e, is_dpop = extracted.is_dpop, "Token validation failed in prepare_repo_write");
85
let mut response = ApiError::from(e).into_response();
86
-
if matches!(e, crate::auth::TokenValidationError::TokenExpired) {
87
-
let scheme = if extracted.is_dpop { "DPoP" } else { "Bearer" };
88
-
let www_auth = format!(
89
-
"{} error=\"invalid_token\", error_description=\"Token has expired\"",
90
-
scheme
91
-
);
92
response.headers_mut().insert(
93
"WWW-Authenticate",
94
www_auth.parse().unwrap(),
95
);
96
-
if extracted.is_dpop {
97
-
let nonce = crate::oauth::verify::generate_dpop_nonce();
98
-
response.headers_mut().insert("DPoP-Nonce", nonce.parse().unwrap());
99
-
}
100
}
101
response
102
})?;
···
322
.await
323
{
324
Ok(res) => res,
325
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
326
};
327
···
580
.await
581
{
582
Ok(res) => res,
583
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
584
};
585
···
83
.map_err(|e| {
84
tracing::warn!(error = ?e, is_dpop = extracted.is_dpop, "Token validation failed in prepare_repo_write");
85
let mut response = ApiError::from(e).into_response();
86
+
if matches!(e, crate::auth::TokenValidationError::TokenExpired) && extracted.is_dpop {
87
+
*response.status_mut() = axum::http::StatusCode::UNAUTHORIZED;
88
+
let www_auth =
89
+
"DPoP error=\"invalid_token\", error_description=\"Token has expired\"";
90
response.headers_mut().insert(
91
"WWW-Authenticate",
92
www_auth.parse().unwrap(),
93
);
94
+
let nonce = crate::oauth::verify::generate_dpop_nonce();
95
+
response.headers_mut().insert("DPoP-Nonce", nonce.parse().unwrap());
96
}
97
response
98
})?;
···
318
.await
319
{
320
Ok(res) => res,
321
+
Err(e) if e.contains("ConcurrentModification") => {
322
+
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
323
+
}
324
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
325
};
326
···
579
.await
580
{
581
Ok(res) => res,
582
+
Err(e) if e.contains("ConcurrentModification") => {
583
+
return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
584
+
}
585
Err(e) => return ApiError::InternalError(Some(e)).into_response(),
586
};
587
+1
src/api/server/mod.rs
+1
src/api/server/mod.rs
+84
src/api/server/password.rs
+84
src/api/server/password.rs
···
412
info!(did = %&auth.0.did, "Password removed - account is now passkey-only");
413
SuccessResponse::ok().into_response()
414
}
415
+
416
+
#[derive(Deserialize)]
417
+
#[serde(rename_all = "camelCase")]
418
+
pub struct SetPasswordInput {
419
+
pub new_password: PlainPassword,
420
+
}
421
+
422
+
pub async fn set_password(
423
+
State(state): State<AppState>,
424
+
auth: BearerAuth,
425
+
Json(input): Json<SetPasswordInput>,
426
+
) -> Response {
427
+
if crate::api::server::reauth::check_reauth_required_cached(
428
+
&state.db,
429
+
&state.cache,
430
+
&auth.0.did,
431
+
)
432
+
.await
433
+
{
434
+
return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await;
435
+
}
436
+
437
+
let new_password = &input.new_password;
438
+
if new_password.is_empty() {
439
+
return ApiError::InvalidRequest("newPassword is required".into()).into_response();
440
+
}
441
+
if let Err(e) = validate_password(new_password) {
442
+
return ApiError::InvalidRequest(e.to_string()).into_response();
443
+
}
444
+
445
+
let user = sqlx::query!(
446
+
"SELECT id, password_hash FROM users WHERE did = $1",
447
+
&auth.0.did
448
+
)
449
+
.fetch_optional(&state.db)
450
+
.await;
451
+
452
+
let user = match user {
453
+
Ok(Some(u)) => u,
454
+
Ok(None) => {
455
+
return ApiError::AccountNotFound.into_response();
456
+
}
457
+
Err(e) => {
458
+
error!("DB error: {:?}", e);
459
+
return ApiError::InternalError(None).into_response();
460
+
}
461
+
};
462
+
463
+
if user.password_hash.is_some() {
464
+
return ApiError::InvalidRequest(
465
+
"Account already has a password. Use changePassword instead.".into(),
466
+
)
467
+
.into_response();
468
+
}
469
+
470
+
let new_password_clone = new_password.to_string();
471
+
let new_hash =
472
+
match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await {
473
+
Ok(Ok(h)) => h,
474
+
Ok(Err(e)) => {
475
+
error!("Failed to hash password: {:?}", e);
476
+
return ApiError::InternalError(None).into_response();
477
+
}
478
+
Err(e) => {
479
+
error!("Failed to spawn blocking task: {:?}", e);
480
+
return ApiError::InternalError(None).into_response();
481
+
}
482
+
};
483
+
484
+
if let Err(e) = sqlx::query!(
485
+
"UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2",
486
+
new_hash,
487
+
user.id
488
+
)
489
+
.execute(&state.db)
490
+
.await
491
+
{
492
+
error!("DB error setting password: {:?}", e);
493
+
return ApiError::InternalError(None).into_response();
494
+
}
495
+
496
+
info!(did = %&auth.0.did, "Password set for passkey-only account");
497
+
SuccessResponse::ok().into_response()
498
+
}
+14
-1
src/lib.rs
+14
-1
src/lib.rs
···
198
post(api::server::remove_password),
199
)
200
.route(
201
"/_account.getPasswordStatus",
202
get(api::server::get_password_status),
203
)
···
590
CorsLayer::new()
591
.allow_origin(Any)
592
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
593
-
.allow_headers(Any)
594
.expose_headers([
595
"WWW-Authenticate".parse().unwrap(),
596
"DPoP-Nonce".parse().unwrap(),
···
198
post(api::server::remove_password),
199
)
200
.route(
201
+
"/_account.setPassword",
202
+
post(api::server::set_password),
203
+
)
204
+
.route(
205
"/_account.getPasswordStatus",
206
get(api::server::get_password_status),
207
)
···
594
CorsLayer::new()
595
.allow_origin(Any)
596
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
597
+
.allow_headers([
598
+
"Authorization".parse().unwrap(),
599
+
"Content-Type".parse().unwrap(),
600
+
"Content-Encoding".parse().unwrap(),
601
+
"Accept-Encoding".parse().unwrap(),
602
+
"DPoP".parse().unwrap(),
603
+
"atproto-proxy".parse().unwrap(),
604
+
"atproto-accept-labelers".parse().unwrap(),
605
+
"x-bsky-topics".parse().unwrap(),
606
+
])
607
.expose_headers([
608
"WWW-Authenticate".parse().unwrap(),
609
"DPoP-Nonce".parse().unwrap(),
-3
src/oauth/endpoints/token/grants.rs
-3
src/oauth/endpoints/token/grants.rs
···
116
} else {
117
None
118
};
119
-
if let Err(e) = db::revoke_tokens_for_client(&state.db, &did, &auth_request.client_id).await {
120
-
tracing::warn!("Failed to revoke previous tokens for client: {:?}", e);
121
-
}
122
let token_id = TokenId::generate();
123
let refresh_token = RefreshToken::generate();
124
let now = Utc::now();
+10
-36
src/scheduled.rs
+10
-36
src/scheduled.rs
···
803
db: &PgPool,
804
block_store: &PostgresBlockStore,
805
user_id: uuid::Uuid,
806
-
head_cid: &Cid,
807
) -> Result<Vec<u8>, String> {
808
-
use jacquard_repo::storage::BlockStore;
809
810
-
let block_cid_bytes: Vec<Vec<u8>> = sqlx::query_scalar!(
811
-
"SELECT block_cid FROM user_blocks WHERE user_id = $1",
812
user_id
813
)
814
-
.fetch_all(db)
815
.await
816
-
.map_err(|e| format!("Failed to fetch user_blocks: {}", e))?;
817
-
818
-
if block_cid_bytes.is_empty() {
819
-
let cids = collect_current_repo_blocks(block_store, head_cid).await?;
820
-
if cids.is_empty() {
821
-
return Err("No blocks found for repo".to_string());
822
-
}
823
-
return generate_repo_car(block_store, head_cid).await;
824
-
}
825
-
826
-
let block_cids: Vec<Cid> = block_cid_bytes
827
-
.iter()
828
-
.filter_map(|bytes| Cid::try_from(bytes.as_slice()).ok())
829
-
.collect();
830
831
-
let car_bytes =
832
-
encode_car_header(head_cid).map_err(|e| format!("Failed to encode CAR header: {}", e))?;
833
834
-
let blocks = block_store
835
-
.get_many(&block_cids)
836
-
.await
837
-
.map_err(|e| format!("Failed to fetch blocks: {:?}", e))?;
838
-
839
-
let car_bytes = block_cids
840
-
.iter()
841
-
.zip(blocks.iter())
842
-
.filter_map(|(cid, block_opt)| block_opt.as_ref().map(|block| (cid, block)))
843
-
.fold(car_bytes, |mut acc, (cid, block)| {
844
-
acc.extend(encode_car_block(cid, block));
845
-
acc
846
-
});
847
-
848
-
Ok(car_bytes)
849
}
850
851
pub async fn generate_full_backup(
···
803
db: &PgPool,
804
block_store: &PostgresBlockStore,
805
user_id: uuid::Uuid,
806
+
_head_cid: &Cid,
807
) -> Result<Vec<u8>, String> {
808
+
use std::str::FromStr;
809
810
+
let repo_root_cid_str: String = sqlx::query_scalar!(
811
+
"SELECT repo_root_cid FROM repos WHERE user_id = $1",
812
user_id
813
)
814
+
.fetch_optional(db)
815
.await
816
+
.map_err(|e| format!("Failed to fetch repo: {}", e))?
817
+
.ok_or_else(|| "Repository not found".to_string())?;
818
819
+
let actual_head_cid = Cid::from_str(&repo_root_cid_str)
820
+
.map_err(|e| format!("Invalid repo_root_cid: {}", e))?;
821
822
+
generate_repo_car(block_store, &actual_head_cid).await
823
}
824
825
pub async fn generate_full_backup(
+2
tests/common/mod.rs
+2
tests/common/mod.rs
···
136
);
137
std::env::set_var("S3_ENDPOINT", &s3_endpoint);
138
std::env::set_var("MAX_IMPORT_SIZE", "100000000");
139
}
140
let mock_server = MockServer::start().await;
141
setup_mock_appview(&mock_server).await;
···
170
std::env::set_var("AWS_REGION", "us-east-1");
171
std::env::set_var("S3_ENDPOINT", &s3_endpoint);
172
std::env::set_var("MAX_IMPORT_SIZE", "100000000");
173
}
174
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
175
.region("us-east-1")
···
136
);
137
std::env::set_var("S3_ENDPOINT", &s3_endpoint);
138
std::env::set_var("MAX_IMPORT_SIZE", "100000000");
139
+
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
140
}
141
let mock_server = MockServer::start().await;
142
setup_mock_appview(&mock_server).await;
···
171
std::env::set_var("AWS_REGION", "us-east-1");
172
std::env::set_var("S3_ENDPOINT", &s3_endpoint);
173
std::env::set_var("MAX_IMPORT_SIZE", "100000000");
174
+
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
175
}
176
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
177
.region("us-east-1")
+1
-1
tests/delete_account.rs
+1
-1
tests/delete_account.rs
+15
-5
tests/import_verification.rs
+15
-5
tests/import_verification.rs
···
156
.send()
157
.await
158
.expect("Failed to import repo");
159
-
assert_eq!(import_res.status(), StatusCode::OK);
160
}
161
162
#[tokio::test]
···
285
async fn test_import_preserves_records_after_reimport() {
286
let client = client();
287
let (token, did) = create_account_and_login(&client).await;
288
-
let mut rkeys = Vec::new();
289
for i in 0..3 {
290
let post_payload = json!({
291
"repo": did,
···
309
assert_eq!(res.status(), StatusCode::OK);
310
let body: serde_json::Value = res.json().await.unwrap();
311
let uri = body["uri"].as_str().unwrap();
312
-
let rkey = uri.split('/').next_back().unwrap().to_string();
313
-
rkeys.push(rkey);
314
}
315
for rkey in &rkeys {
316
let get_res = client
···
352
.send()
353
.await
354
.expect("Failed to import repo");
355
-
assert_eq!(import_res.status(), StatusCode::OK);
356
let list_res = client
357
.get(format!(
358
"{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
···
156
.send()
157
.await
158
.expect("Failed to import repo");
159
+
let status = import_res.status();
160
+
if status != StatusCode::OK {
161
+
let body = import_res.text().await.unwrap_or_default();
162
+
panic!(
163
+
"Import failed with status {}: {}",
164
+
status, body
165
+
);
166
+
}
167
}
168
169
#[tokio::test]
···
292
async fn test_import_preserves_records_after_reimport() {
293
let client = client();
294
let (token, did) = create_account_and_login(&client).await;
295
+
let mut rkeys = Vec::with_capacity(3);
296
for i in 0..3 {
297
let post_payload = json!({
298
"repo": did,
···
316
assert_eq!(res.status(), StatusCode::OK);
317
let body: serde_json::Value = res.json().await.unwrap();
318
let uri = body["uri"].as_str().unwrap();
319
+
rkeys.push(uri.split('/').next_back().unwrap().to_string());
320
}
321
for rkey in &rkeys {
322
let get_res = client
···
358
.send()
359
.await
360
.expect("Failed to import repo");
361
+
let status = import_res.status();
362
+
if status != StatusCode::OK {
363
+
let body = import_res.text().await.unwrap_or_default();
364
+
panic!("Import failed with status {}: {}", status, body);
365
+
}
366
let list_res = client
367
.get(format!(
368
"{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
+1
-1
tests/password_reset.rs
+1
-1
tests/password_reset.rs