diff --git a/.sqlx/query-3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4.json b/.sqlx/query-3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4.json new file mode 100644 index 0000000..5313629 --- /dev/null +++ b/.sqlx/query-3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = $1, password_required = TRUE WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "3a8b7a2773033c85cd03dd6f906a9d335019f9b7145e1bee6175d01d0c98b8b4" +} diff --git a/.sqlx/query-e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978.json b/.sqlx/query-e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978.json deleted file mode 100644 index dfb96ec..0000000 --- a/.sqlx/query-e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT block_cid FROM user_blocks WHERE user_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "block_cid", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "e70fc3dced4eb7dc220ca2a18cdfcbd5f2d66dff2262bb083fd4118b032ff978" -} diff --git a/frontend/src/components/ReauthModal.svelte b/frontend/src/components/ReauthModal.svelte index d69a7e6..097fb5a 100644 --- a/frontend/src/components/ReauthModal.svelte +++ b/frontend/src/components/ReauthModal.svelte @@ -330,7 +330,7 @@ .tab.active { background: var(--accent); border-color: var(--accent); - color: white; + color: var(--text-inverse); } .modal-content { @@ -375,7 +375,7 @@ width: 100%; padding: 0.75rem 1.5rem; background: var(--accent); - color: white; + color: var(--text-inverse); border: none; border-radius: 4px; font-size: 1rem; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5c9617f..ee53039 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -540,6 +540,14 @@ export const api = { }); }, + setPassword(token: AccessToken, newPassword: string): Promise { + return xrpc("_account.setPassword", { + method: "POST", + token, + body: { newPassword }, + }); + }, + getPasswordStatus(token: AccessToken): Promise { return xrpc("_account.getPasswordStatus", { token }); }, diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 1446702..a1e6267 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -303,6 +303,10 @@ "confirmNewPasswordPlaceholder": "Confirm new password", "changePasswordButton": "Change Password", "changing": "Changing...", + "setPassword": "Set Password", + "setPasswordDescription": "Your account is currently passkey-only. You can add a password to enable traditional login alongside your passkeys.", + "setPasswordButton": "Set Password", + "setting": "Setting...", "exportData": "Export Data", "exportDataDescription": "Download your entire repository as a CAR (Content Addressable Archive) file. This includes all your posts, likes, follows, and other data.", "downloadRepo": "Download Repository", @@ -352,6 +356,8 @@ "handleUpdateFailed": "Failed to update handle", "passwordChanged": "Password changed successfully", "passwordChangeFailed": "Failed to change password", + "passwordSet": "Password set successfully", + "passwordSetFailed": "Failed to set password", "passwordsMismatch": "Passwords do not match", "passwordsDoNotMatch": "Passwords do not match", "passwordLength": "Password must be at least 8 characters", @@ -509,6 +515,8 @@ "beforeProceedingItem3": "Ensure your recovery notification channel is up to date", "addPasskeyFirst": "Add at least one passkey before you can remove your password.", "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.", + "addPasswordHint": "Want to add a password? Go to Settings to set one up.", + "goToSettings": "Go to Settings", "trustedDevices": "Trusted Devices", "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.", "manageTrustedDevices": "Manage Trusted Devices", diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json index 269bd69..2174bd4 100644 --- a/frontend/src/locales/fi.json +++ b/frontend/src/locales/fi.json @@ -303,6 +303,10 @@ "confirmNewPasswordPlaceholder": "Vahvista uusi salasana", "changePasswordButton": "Vaihda salasana", "changing": "Vaihdetaan...", + "setPassword": "Aseta salasana", + "setPasswordDescription": "Tilisi on tällä hetkellä vain pääsyavain-tili. Voit lisätä salasanan ottaaksesi käyttöön perinteisen kirjautumisen pääsyavainten rinnalla.", + "setPasswordButton": "Aseta salasana", + "setting": "Asetetaan...", "exportData": "Vie tiedot", "exportDataDescription": "Lataa koko tietovarastosi CAR-tiedostona (Content Addressable Archive). Tämä sisältää kaikki julkaisusi, tykkäyksesi, seuraamisesi ja muut tiedot.", "downloadRepo": "Lataa tietovarasto", @@ -352,6 +356,8 @@ "handleUpdateFailed": "Käyttäjänimen päivitys epäonnistui", "passwordChanged": "Salasana vaihdettu", "passwordChangeFailed": "Salasanan vaihto epäonnistui", + "passwordSet": "Salasana asetettu onnistuneesti", + "passwordSetFailed": "Salasanan asettaminen epäonnistui", "passwordsMismatch": "Salasanat eivät täsmää", "passwordsDoNotMatch": "Salasanat eivät täsmää", "passwordLength": "Salasanan on oltava vähintään 8 merkkiä", @@ -509,6 +515,8 @@ "beforeProceedingItem3": "Varmista, että palautusilmoituskanavasi on ajan tasalla", "addPasskeyFirst": "Lisää vähintään yksi pääsyavain ennen kuin voit poistaa salasanasi.", "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.", + "addPasswordHint": "Haluatko lisätä salasanan? Siirry Asetuksiin määrittääksesi sellaisen.", + "goToSettings": "Siirry asetuksiin", "trustedDevices": "Luotetut laitteet", "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.", "manageTrustedDevices": "Hallitse luotettuja laitteita", diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index a55d10e..86bf2ed 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -296,6 +296,10 @@ "confirmNewPasswordPlaceholder": "新しいパスワードを再入力", "changePasswordButton": "パスワードを変更", "changing": "変更中...", + "setPassword": "パスワードを設定", + "setPasswordDescription": "現在、あなたのアカウントはパスキーのみです。パスワードを追加すると、パスキーと併せて従来のログインも使用できます。", + "setPasswordButton": "パスワードを設定", + "setting": "設定中...", "exportData": "データエクスポート", "exportDataDescription": "リポジトリ全体を CAR(Content Addressable Archive)ファイルとしてダウンロードします。投稿、いいね、フォローなどすべてのデータが含まれます。", "downloadRepo": "リポジトリをダウンロード", @@ -345,6 +349,8 @@ "handleUpdateFailed": "ハンドルの更新に失敗しました", "passwordChanged": "パスワードを変更しました", "passwordChangeFailed": "パスワードの変更に失敗しました", + "passwordSet": "パスワードを設定しました", + "passwordSetFailed": "パスワードの設定に失敗しました", "passwordsMismatch": "パスワードが一致しません", "passwordsDoNotMatch": "パスワードが一致しません", "passwordLength": "パスワードは8文字以上である必要があります", @@ -502,6 +508,8 @@ "beforeProceedingItem3": "復旧用の通知チャンネルが最新であることを確認", "addPasskeyFirst": "パスワードを削除する前に、少なくとも1つのパスキーを追加してください。", "passkeyOnlyHint": "パスキーのみでサインインしています。パスキーにアクセスできなくなった場合、ログインページの「パスキーを紛失しましたか?」リンクからアカウントを復旧できます。", + "addPasswordHint": "パスワードを追加しますか?設定で追加できます。", + "goToSettings": "設定へ移動", "trustedDevices": "信頼済みデバイス", "trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイスを管理します。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", "manageTrustedDevices": "信頼済みデバイスを管理", diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 20faa0f..95ae6b0 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -296,6 +296,10 @@ "confirmNewPasswordPlaceholder": "새 비밀번호 재입력", "changePasswordButton": "비밀번호 변경", "changing": "변경 중...", + "setPassword": "비밀번호 설정", + "setPasswordDescription": "현재 계정은 패스키 전용입니다. 비밀번호를 추가하면 패스키와 함께 기존 로그인 방식도 사용할 수 있습니다.", + "setPasswordButton": "비밀번호 설정", + "setting": "설정 중...", "exportData": "데이터 내보내기", "exportDataDescription": "전체 저장소를 CAR (Content Addressable Archive) 파일로 다운로드합니다. 모든 게시물, 좋아요, 팔로우 및 기타 데이터가 포함됩니다.", "downloadRepo": "저장소 다운로드", @@ -345,6 +349,8 @@ "handleUpdateFailed": "핸들 업데이트에 실패했습니다", "passwordChanged": "비밀번호가 변경되었습니다", "passwordChangeFailed": "비밀번호 변경에 실패했습니다", + "passwordSet": "비밀번호가 설정되었습니다", + "passwordSetFailed": "비밀번호 설정에 실패했습니다", "passwordsMismatch": "비밀번호가 일치하지 않습니다", "passwordsDoNotMatch": "비밀번호가 일치하지 않습니다", "passwordLength": "비밀번호는 8자 이상이어야 합니다", @@ -502,6 +508,8 @@ "beforeProceedingItem3": "복구 알림 채널이 최신인지 확인", "addPasskeyFirst": "비밀번호를 제거하려면 먼저 최소 하나의 패스키를 추가하세요.", "passkeyOnlyHint": "패스키로만 로그인합니다. 패스키에 액세스할 수 없게 되면 로그인 페이지의 '패스키를 분실하셨나요?' 링크를 사용하여 계정을 복구할 수 있습니다.", + "addPasswordHint": "비밀번호를 추가하시겠습니까? 설정에서 설정하세요.", + "goToSettings": "설정으로 이동", "trustedDevices": "신뢰할 수 있는 기기", "trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기를 관리합니다. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.", "manageTrustedDevices": "신뢰할 수 있는 기기 관리", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index d58985e..1f810cc 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -296,6 +296,10 @@ "confirmNewPasswordPlaceholder": "Bekräfta nytt lösenord", "changePasswordButton": "Ändra lösenord", "changing": "Ändrar...", + "setPassword": "Ange lösenord", + "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.", + "setPasswordButton": "Ange lösenord", + "setting": "Anger...", "exportData": "Exportera data", "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.", "downloadRepo": "Ladda ner arkiv", @@ -345,6 +349,8 @@ "handleUpdateFailed": "Kunde inte uppdatera användarnamn", "passwordChanged": "Lösenord ändrat", "passwordChangeFailed": "Kunde inte ändra lösenord", + "passwordSet": "Lösenord har angetts", + "passwordSetFailed": "Kunde inte ange lösenord", "passwordsMismatch": "Lösenorden matchar inte", "passwordsDoNotMatch": "Lösenorden matchar inte", "passwordLength": "Lösenordet måste vara minst 8 tecken", @@ -502,6 +508,8 @@ "beforeProceedingItem3": "Se till att din meddelandekanal för återställning är uppdaterad", "addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.", "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.", + "addPasswordHint": "Vill du lägga till ett lösenord? Gå till Inställningar för att ställa in ett.", + "goToSettings": "Gå till inställningar", "trustedDevices": "Betrodda enheter", "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.", "manageTrustedDevices": "Hantera betrodda enheter", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 792c1c8..7079195 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -296,6 +296,10 @@ "confirmNewPasswordPlaceholder": "再次输入新密码", "changePasswordButton": "更改密码", "changing": "更改中...", + "setPassword": "设置密码", + "setPasswordDescription": "您的账户当前仅使用通行密钥。您可以添加密码以启用传统登录方式与通行密钥并用。", + "setPasswordButton": "设置密码", + "setting": "设置中...", "exportData": "导出数据", "exportDataDescription": "将您的所有数据下载为 CAR 文件。包括您的所有帖子、点赞、关注等数据。", "downloadRepo": "下载数据", @@ -345,6 +349,8 @@ "handleUpdateFailed": "用户名更新失败", "passwordChanged": "密码更改成功", "passwordChangeFailed": "密码更改失败", + "passwordSet": "密码设置成功", + "passwordSetFailed": "密码设置失败", "passwordsMismatch": "两次输入的密码不一致", "passwordsDoNotMatch": "两次输入的密码不一致", "passwordLength": "密码至少需要8位字符", @@ -502,6 +508,8 @@ "beforeProceedingItem3": "确保您的恢复通知渠道是最新的", "addPasskeyFirst": "请先添加至少一个通行密钥才能移除密码。", "passkeyOnlyHint": "您使用通行密钥登录。如果您丢失了通行密钥,可以使用登录页面上的「丢失通行密钥?」链接恢复账户。", + "addPasswordHint": "想要添加密码?前往设置进行设置。", + "goToSettings": "前往设置", "trustedDevices": "受信任设备", "trustedDevicesDescription": "管理可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。", "manageTrustedDevices": "管理受信任设备", diff --git a/frontend/src/routes/Security.svelte b/frontend/src/routes/Security.svelte index f3b600c..86444d8 100644 --- a/frontend/src/routes/Security.svelte +++ b/frontend/src/routes/Security.svelte @@ -678,6 +678,12 @@

{$_('security.passkeyOnlyHint')}

+

+ {$_('security.addPasswordHint')} +

+ + {$_('security.goToSettings')} + {/if} diff --git a/frontend/src/routes/Settings.svelte b/frontend/src/routes/Settings.svelte index 9cc29ce..448c838 100644 --- a/frontend/src/routes/Settings.svelte +++ b/frontend/src/routes/Settings.svelte @@ -8,6 +8,7 @@ import { unsafeAsHandle } from '../lib/types/branded' import type { Session } from '../lib/types/api' import { toast } from '../lib/toast.svelte' + import ReauthModal from '../components/ReauthModal.svelte' const auth = $derived(getAuthState()) const supportedLocales = getSupportedLocales() @@ -63,6 +64,12 @@ let newPassword = $state('') let confirmNewPassword = $state('') let showBYOHandle = $state(false) + let hasPassword = $state(true) + let passwordStatusLoading = $state(true) + let setPasswordLoading = $state(false) + let showReauthModal = $state(false) + let reauthMethods = $state(['passkey']) + let pendingAction = $state<(() => Promise) | null>(null) $effect(() => { if (!loading && !session) { @@ -70,6 +77,25 @@ } }) + $effect(() => { + if (session) { + loadPasswordStatus() + } + }) + + async function loadPasswordStatus() { + if (!session) return + passwordStatusLoading = true + try { + const status = await api.getPasswordStatus(session.accessJwt) + hasPassword = status.hasPassword + } catch { + hasPassword = true + } finally { + passwordStatusLoading = false + } + } + async function handleRequestEmailUpdate() { if (!session) return emailLoading = true @@ -374,6 +400,52 @@ passwordLoading = false } } + + async function handleSetPassword(e: Event) { + e.preventDefault() + if (!session || !newPassword || !confirmNewPassword) return + if (newPassword !== confirmNewPassword) { + toast.error($_('settings.messages.passwordsDoNotMatch')) + return + } + if (newPassword.length < 8) { + toast.error($_('settings.messages.passwordTooShort')) + return + } + setPasswordLoading = true + try { + await api.setPassword(session.accessJwt, newPassword) + toast.success($_('settings.messages.passwordSet')) + hasPassword = true + newPassword = '' + confirmNewPassword = '' + } catch (e) { + if (e instanceof ApiError) { + if (e.error === 'ReauthRequired') { + reauthMethods = e.reauthMethods || ['passkey'] + pendingAction = () => handleSetPassword(new Event('submit')) + showReauthModal = true + } else { + toast.error(e.message) + } + } else { + toast.error($_('settings.messages.passwordSetFailed')) + } + } finally { + setPasswordLoading = false + } + } + + function handleReauthSuccess() { + if (pendingAction) { + pendingAction() + pendingAction = null + } + } + + function handleReauthCancel() { + pendingAction = null + }
@@ -522,48 +594,85 @@ {/if} -
-

{$_('settings.changePassword')}

-
-
- - -
-
- - -
-
- - -
- -
-
+ {#if !passwordStatusLoading} + {#if hasPassword} +
+

{$_('settings.changePassword')}

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ {:else} +
+

{$_('settings.setPassword')}

+

{$_('settings.setPasswordDescription')}

+
+
+ + +
+
+ + +
+ +
+
+ {/if} + {/if}

{$_('settings.exportData')}

{$_('settings.exportDataDescription')}

@@ -681,6 +790,15 @@ {/if}
+ +{#if showReauthModal && session} + +{/if}