From 83675944ef11fcdc6bc541ff5e9a0b20967c41ef Mon Sep 17 00:00:00 2001 From: scanash00 Date: Tue, 25 Aug 2026 03:23:17 -0800 Subject: [PATCH] fixes --- crates/tranquil-api/src/lib.rs | 4 + .../tranquil-api/src/server/app_password.rs | 167 +++++++++++++- crates/tranquil-api/src/server/mod.rs | 4 +- crates/tranquil-db-traits/src/lib.rs | 2 +- crates/tranquil-db-traits/src/session.rs | 10 + crates/tranquil-db/src/postgres/session.rs | 23 +- crates/tranquil-store/src/metastore/client.rs | 14 ++ .../tranquil-store/src/metastore/handler.rs | 16 ++ .../src/metastore/session_ops.rs | 45 +++- .../dashboard/AppPasswordsContent.svelte | 207 +++++++++++++++--- frontend/src/lib/api.ts | 12 + frontend/src/lib/oauthConsent.ts | 32 +++ frontend/src/lib/types/api.ts | 10 +- frontend/src/locales/en.json | 3 + frontend/src/locales/fi.json | 3 + frontend/src/locales/fr.json | 3 + frontend/src/locales/ja.json | 3 + frontend/src/locales/ko.json | 3 + frontend/src/locales/sv.json | 3 + frontend/src/locales/zh.json | 3 + frontend/src/routes/OAuthConsent.svelte | 16 +- frontend/src/styles/dashboard.css | 55 ++++- frontend/src/tests/oauthConsent.test.ts | 57 +++++ 23 files changed, 640 insertions(+), 55 deletions(-) create mode 100644 frontend/src/lib/oauthConsent.ts create mode 100644 frontend/src/tests/oauthConsent.test.ts diff --git a/crates/tranquil-api/src/lib.rs b/crates/tranquil-api/src/lib.rs index 6f37d98..e750b1f 100644 --- a/crates/tranquil-api/src/lib.rs +++ b/crates/tranquil-api/src/lib.rs @@ -301,6 +301,10 @@ pub fn api_routes() -> axum::Router { "/com.atproto.server.revokeAppPassword", post(server::revoke_app_password), ) + .route( + "/_account.updateAppPassword", + post(server::update_app_password), + ) .route( "/com.atproto.server.createInviteCode", post(server::create_invite_code), diff --git a/crates/tranquil-api/src/server/app_password.rs b/crates/tranquil-api/src/server/app_password.rs index 2c548c5..6e16aa3 100644 --- a/crates/tranquil-api/src/server/app_password.rs +++ b/crates/tranquil-api/src/server/app_password.rs @@ -1,7 +1,7 @@ use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use serde_json::json; -use tranquil_db_traits::AppPasswordCreate; +use tranquil_db_traits::{AppPasswordCreate, AppPasswordUpdate}; use tranquil_pds::api::EmptyResponse; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::auth::{Auth, NotTakendown, Permissive, generate_app_password}; @@ -209,6 +209,171 @@ pub async fn create_app_password( })) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateAppPasswordInput { + pub current_name: String, + pub name: String, + pub scopes: Option, +} + +pub async fn update_app_password( + State(state): State, + auth: Auth, + Json(input): Json, +) -> Result, ApiError> { + let user = state + .repos + .user + .get_by_did(&auth.did) + .await + .log_db_err("getting user")? + .ok_or(ApiError::AccountNotFound)?; + + let current_name = input.current_name.trim(); + if current_name.is_empty() { + return Err(ApiError::InvalidRequest("currentName is required".into())); + } + + let name = input.name.trim(); + if name.is_empty() { + return Err(ApiError::InvalidRequest("name is required".into())); + } + + let existing = state + .repos + .session + .get_app_password_by_name(user.id, current_name) + .await + .log_db_err("getting app password")? + .ok_or(ApiError::AppPasswordNotFound)?; + + if let Some(ref controller) = auth.controller_did + && existing.created_by_controller_did.as_ref() != Some(controller) + { + return Err(ApiError::Forbidden); + } + + if name != current_name + && state + .repos + .session + .get_app_password_by_name(user.id, name) + .await + .log_db_err("checking app password rename")? + .is_some() + { + return Err(ApiError::DuplicateAppPassword); + } + + let final_scopes = if let Some(ref controller) = auth.controller_did { + let grant = state + .repos + .delegation + .get_delegation(&auth.did, controller) + .await + .ok() + .flatten(); + let granted_scopes = match grant { + Some(g) => g.granted_scopes, + None => return Err(ApiError::InsufficientScope(None)), + }; + + let requested = match input.scopes.as_deref() { + Some(scopes) => with_required_atproto_scope(scopes), + None => with_required_atproto_scope(granted_scopes.as_str()), + }; + let intersected = intersect_scopes(&requested, granted_scopes.as_str()); + + if intersected.is_empty() && !granted_scopes.is_empty() { + return Err(ApiError::InsufficientScope(None)); + } + + if intersected.is_empty() { + None + } else { + Some(intersected) + } + } else { + let scopes = input + .scopes + .as_deref() + .unwrap_or("transition:generic transition:chat.bsky"); + Some(with_required_atproto_scope(scopes)) + }; + + let sessions_to_invalidate = state + .repos + .session + .get_session_jtis_by_app_password(&auth.did, current_name) + .await + .log_db_err("getting sessions for app password update")?; + + state + .repos + .session + .delete_sessions_by_app_password(&auth.did, current_name) + .await + .log_db_err("revoking sessions for app password update")?; + + futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { + let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, jti); + let cache = state.cache.clone(); + async move { + let _ = cache.delete(&cache_key).await; + } + })) + .await; + + let update = AppPasswordUpdate { + user_id: user.id, + current_name: current_name.to_string(), + name: name.to_string(), + scopes: final_scopes.clone(), + }; + let rows_affected = state + .repos + .session + .update_app_password(&update) + .await + .log_db_err("updating app password")?; + if rows_affected == 0 { + return Err(ApiError::AppPasswordNotFound); + } + + if let Some(ref controller) = auth.controller_did { + let _ = state + .repos + .delegation + .log_delegation_action( + &auth.did, + controller, + Some(controller), + DelegationActionType::AccountAction, + Some(json!({ + "action": "update_app_password", + "current_name": current_name, + "name": name, + "scopes": final_scopes + })), + None, + None, + ) + .await; + } + + Ok(Json(AppPassword { + name: name.to_string(), + created_at: existing.created_at.to_rfc3339(), + privileged: existing.privilege.is_privileged(), + scopes: final_scopes, + created_by_controller: existing + .created_by_controller_did + .as_ref() + .map(|did| did.to_string()), + })) +} + #[derive(Deserialize)] pub struct RevokeAppPasswordInput { pub name: String, diff --git a/crates/tranquil-api/src/server/mod.rs b/crates/tranquil-api/src/server/mod.rs index d43cf26..1f3aa22 100644 --- a/crates/tranquil-api/src/server/mod.rs +++ b/crates/tranquil-api/src/server/mod.rs @@ -21,7 +21,9 @@ pub use account_status::{ activate_account, check_account_status, deactivate_account, delete_account, request_account_delete, }; -pub use app_password::{create_app_password, list_app_passwords, revoke_app_password}; +pub use app_password::{ + create_app_password, list_app_passwords, revoke_app_password, update_app_password, +}; pub use email::{ authorize_email_update, check_channel_verified, check_email_in_use, check_email_update_status, check_email_verified, confirm_email, request_email_update, update_email, diff --git a/crates/tranquil-db-traits/src/lib.rs b/crates/tranquil-db-traits/src/lib.rs index ff9c630..2ccf7a1 100644 --- a/crates/tranquil-db-traits/src/lib.rs +++ b/crates/tranquil-db-traits/src/lib.rs @@ -43,7 +43,7 @@ pub use repo::{ pub use scope::{DbScope, InvalidScopeError}; pub use sequence::{SequenceNumber, deserialize_optional_sequence}; pub use session::{ - AppPasswordCreate, AppPasswordPrivilege, AppPasswordRecord, LoginType, + AppPasswordCreate, AppPasswordPrivilege, AppPasswordRecord, AppPasswordUpdate, LoginType, REFRESH_GRACE_PERIOD_SECS, RefreshGraceLookup, RefreshGraceReplay, RefreshSessionResult, SessionForRefresh, SessionId, SessionListItem, SessionMfaStatus, SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate, diff --git a/crates/tranquil-db-traits/src/session.rs b/crates/tranquil-db-traits/src/session.rs index 9102dbd..50754e1 100644 --- a/crates/tranquil-db-traits/src/session.rs +++ b/crates/tranquil-db-traits/src/session.rs @@ -151,6 +151,14 @@ pub struct AppPasswordCreate { pub created_by_controller_did: Option, } +#[derive(Debug, Clone)] +pub struct AppPasswordUpdate { + pub user_id: Uuid, + pub current_name: String, + pub name: String, + pub scopes: Option, +} + #[derive(Debug, Clone)] pub struct SessionMfaStatus { pub login_type: LoginType, @@ -280,6 +288,8 @@ pub trait SessionRepository: Send + Sync { async fn create_app_password(&self, data: &AppPasswordCreate) -> Result; + async fn update_app_password(&self, data: &AppPasswordUpdate) -> Result; + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result; async fn delete_app_passwords_by_controller( diff --git a/crates/tranquil-db/src/postgres/session.rs b/crates/tranquil-db/src/postgres/session.rs index 0eef369..65524a5 100644 --- a/crates/tranquil-db/src/postgres/session.rs +++ b/crates/tranquil-db/src/postgres/session.rs @@ -2,10 +2,10 @@ use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; use sqlx::PgPool; use tranquil_db_traits::{ - AppPasswordCreate, AppPasswordPrivilege, AppPasswordRecord, DbError, LoginType, - REFRESH_GRACE_PERIOD_SECS, RefreshGraceLookup, RefreshGraceReplay, RefreshSessionResult, - SessionForRefresh, SessionId, SessionListItem, SessionMfaStatus, SessionRefreshData, - SessionRepository, SessionToken, SessionTokenCreate, + AppPasswordCreate, AppPasswordPrivilege, AppPasswordRecord, AppPasswordUpdate, DbError, + LoginType, REFRESH_GRACE_PERIOD_SECS, RefreshGraceLookup, RefreshGraceReplay, + RefreshSessionResult, SessionForRefresh, SessionId, SessionListItem, SessionMfaStatus, + SessionRefreshData, SessionRepository, SessionToken, SessionTokenCreate, }; use tranquil_types::{Did, Jti, PasswordHash}; use uuid::Uuid; @@ -429,6 +429,21 @@ impl SessionRepository for PostgresSessionRepository { Ok(row.id) } + async fn update_app_password(&self, data: &AppPasswordUpdate) -> Result { + let result = sqlx::query( + "UPDATE app_passwords SET name = $1, scopes = $2 WHERE user_id = $3 AND name = $4", + ) + .bind(&data.name) + .bind(&data.scopes) + .bind(data.user_id) + .bind(&data.current_name) + .execute(&self.pool) + .await + .map_err(map_sqlx_error)?; + + Ok(result.rows_affected()) + } + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result { let result = sqlx::query!( "DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index d0fc787..ccd081e 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -1679,6 +1679,20 @@ impl tranquil_db_traits::SessionRepository for Metastore recv(rx).await } + async fn update_app_password( + &self, + data: &tranquil_db_traits::AppPasswordUpdate, + ) -> Result { + let (tx, rx) = oneshot::channel(); + self.pool.send(MetastoreRequest::Session( + SessionRequest::UpdateAppPassword { + data: data.clone(), + tx, + }, + ))?; + recv(rx).await + } + async fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result { let (tx, rx) = oneshot::channel(); self.pool.send(MetastoreRequest::Session( diff --git a/crates/tranquil-store/src/metastore/handler.rs b/crates/tranquil-store/src/metastore/handler.rs index 38f34f9..814ae39 100644 --- a/crates/tranquil-store/src/metastore/handler.rs +++ b/crates/tranquil-store/src/metastore/handler.rs @@ -920,6 +920,10 @@ pub enum SessionRequest { data: tranquil_db_traits::AppPasswordCreate, tx: Tx, }, + UpdateAppPassword { + data: tranquil_db_traits::AppPasswordUpdate, + tx: Tx, + }, DeleteAppPassword { user_id: Uuid, name: String, @@ -985,6 +989,10 @@ impl SessionRequest { data: tranquil_db_traits::AppPasswordCreate { user_id, .. }, .. } + | Self::UpdateAppPassword { + data: tranquil_db_traits::AppPasswordUpdate { user_id, .. }, + .. + } | Self::DeleteAppPassword { user_id, .. } => uuid_to_routing(user_hashes, user_id), } } @@ -3852,6 +3860,14 @@ fn dispatch_session(state: &HandlerState, req: SessionRequest) .map_err(metastore_to_db); let _ = tx.send(result); } + SessionRequest::UpdateAppPassword { data, tx } => { + let result = state + .metastore + .session_ops() + .update_app_password(&data) + .map_err(metastore_to_db); + let _ = tx.send(result); + } SessionRequest::DeleteAppPassword { user_id, name, tx } => { let result = state .metastore diff --git a/crates/tranquil-store/src/metastore/session_ops.rs b/crates/tranquil-store/src/metastore/session_ops.rs index 1381a75..dcefae9 100644 --- a/crates/tranquil-store/src/metastore/session_ops.rs +++ b/crates/tranquil-store/src/metastore/session_ops.rs @@ -20,9 +20,9 @@ use super::user_hash::UserHashMap; use super::users::UserValue; use tranquil_db_traits::{ - AppPasswordCreate, AppPasswordRecord, LoginType, REFRESH_GRACE_PERIOD_SECS, RefreshGraceLookup, - RefreshGraceReplay, RefreshSessionResult, SessionForRefresh, SessionId, SessionListItem, - SessionMfaStatus, SessionRefreshData, SessionToken, SessionTokenCreate, + AppPasswordCreate, AppPasswordRecord, AppPasswordUpdate, LoginType, REFRESH_GRACE_PERIOD_SECS, + RefreshGraceLookup, RefreshGraceReplay, RefreshSessionResult, SessionForRefresh, SessionId, + SessionListItem, SessionMfaStatus, SessionRefreshData, SessionToken, SessionTokenCreate, }; use tranquil_types::{Did, Jti, PasswordHash}; @@ -639,6 +639,45 @@ impl SessionOps { Ok(id) } + pub fn update_app_password(&self, data: &AppPasswordUpdate) -> Result { + let user_hash = self.resolve_user_hash_from_uuid(data.user_id)?; + let current_key = session_app_password_key(user_hash, &data.current_name); + let Some(raw) = self + .auth + .get(current_key.as_slice()) + .map_err(MetastoreError::Fjall)? + else { + return Ok(0); + }; + let mut value = AppPasswordValue::deserialize(&raw) + .ok_or(MetastoreError::CorruptData("corrupt app password"))?; + let new_key = session_app_password_key(user_hash, &data.name); + + if current_key.as_slice() != new_key.as_slice() + && self + .auth + .get(new_key.as_slice()) + .map_err(MetastoreError::Fjall)? + .is_some() + { + return Err(MetastoreError::UniqueViolation( + "app_passwords_user_id_name", + )); + } + + value.name.clone_from(&data.name); + value.scopes.clone_from(&data.scopes); + + let mut batch = self.db.batch(); + if current_key.as_slice() != new_key.as_slice() { + batch.remove(&self.auth, current_key.as_slice()); + } + batch.insert(&self.auth, new_key.as_slice(), value.serialize()); + batch.commit().map_err(MetastoreError::Fjall)?; + + Ok(1) + } + pub fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result { let user_hash = self.resolve_user_hash_from_uuid(user_id)?; let key = session_app_password_key(user_hash, name); diff --git a/frontend/src/components/dashboard/AppPasswordsContent.svelte b/frontend/src/components/dashboard/AppPasswordsContent.svelte index 013ce5a..73ee60b 100644 --- a/frontend/src/components/dashboard/AppPasswordsContent.svelte +++ b/frontend/src/components/dashboard/AppPasswordsContent.svelte @@ -1,10 +1,10 @@ @@ -175,6 +233,7 @@ type="button" class="scope-btn" class:selected={selectedScopePreset === preset.id} + aria-pressed={selectedScopePreset === preset.id} onclick={() => selectedScopePreset = preset.id} disabled={creating} > @@ -212,26 +271,114 @@

{$_('appPasswords.noPasswords')}

{:else}
    - {#each appPasswords as pw} + {#each appPasswords as pw, index}
  • -
    - {pw.name} - - {getScopeLabel(pw.scopes)} - {#if pw.createdByController} - {$_('appPasswords.byController')} + {#if editingName === pw.name} +
    +
    +
    + + editError = null} + disabled={savingEdit} + aria-describedby={editError ? `edit-app-password-error-${index}` : undefined} + aria-invalid={editError ? 'true' : undefined} + required + /> +
    +
    + {$_('appPasswords.permissions')}: +
    + {#each SCOPE_PRESETS as preset} + + {/each} +
    + {#if editScopePreset === 'custom'} +
    + + +

    + {$_('appPasswords.scopeCustomHelp')} +

    +
    + {/if} +
    +
    +
    + + +
    + {#if editError} + {/if} - {$_('common.created')} {formatDate(pw.createdAt)} - -
    - + + {:else} +
    + {pw.name} + + {getScopeLabel(pw.scopes)} + {#if pw.createdByController} + {$_('appPasswords.byController')} + {/if} + {$_('common.created')} {formatDate(pw.createdAt)} + +
    +
    + + +
    + {/if}
  • {/each}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index bc6ea74..551aa4c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -79,6 +79,7 @@ import type { SuccessResponse, TotpSecret, TotpStatus, + UpdateAppPasswordParams, UpdateLegacyLoginResponse, UpdateLocaleResponse, UpdateNotificationPrefsResponse, @@ -566,6 +567,17 @@ export const api = { }); }, + updateAppPassword( + token: AccessToken, + params: UpdateAppPasswordParams, + ): Promise { + return xrpc("_account.updateAppPassword", { + method: "POST", + token, + body: params, + }); + }, + async revokeAppPassword(token: AccessToken, name: string): Promise { await xrpc("com.atproto.server.revokeAppPassword", { method: "POST", diff --git a/frontend/src/lib/oauthConsent.ts b/frontend/src/lib/oauthConsent.ts new file mode 100644 index 0000000..5362737 --- /dev/null +++ b/frontend/src/lib/oauthConsent.ts @@ -0,0 +1,32 @@ +export interface RequestedConsentScope { + scope: string; + required: boolean; + granted: boolean | null; + restricted?: boolean; +} + +export interface RequestedPermissionBundle { + include_scope: string; + granted?: boolean | null; + restricted?: boolean; +} + +export function getInitialConsentSelections( + scopes: RequestedConsentScope[], + permissionBundles: RequestedPermissionBundle[], +): Record { + const selections = Object.fromEntries( + scopes + .filter((scope) => !scope.restricted) + .map((scope) => [ + scope.scope, + scope.required ? true : (scope.granted ?? true), + ]), + ); + + for (const bundle of permissionBundles) { + if (!bundle.restricted) selections[bundle.include_scope] = true; + } + + return selections; +} diff --git a/frontend/src/lib/types/api.ts b/frontend/src/lib/types/api.ts index 3920cf6..6fae0a0 100644 --- a/frontend/src/lib/types/api.ts +++ b/frontend/src/lib/types/api.ts @@ -124,8 +124,14 @@ export interface DidDocument { export interface AppPassword { name: string; createdAt: ISODateString; - scopes?: string; - createdByController?: string; + scopes?: string | null; + createdByController?: string | null; +} + +export interface UpdateAppPasswordParams { + currentName: string; + name: string; + scopes: string | null; } export interface CreatedAppPassword { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 954351b..15e4bbf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -298,6 +298,9 @@ "deleted": "App password revoked", "loadFailed": "Failed to load app passwords", "createFailed": "Failed to create app password", + "edit": "Edit", + "updated": "App password updated", + "updateFailed": "Failed to update app password", "deleteFailed": "Failed to revoke app password", "saveWarning": "Save this password now - you will not see it again", "saveWarningTitle": "Important: Save this app password!", diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json index 9229ae0..7c90a75 100644 --- a/frontend/src/locales/fi.json +++ b/frontend/src/locales/fi.json @@ -310,6 +310,9 @@ "deleted": "Sovelluksen salasana peruutettu", "loadFailed": "Sovellusten salasanojen lataus epäonnistui", "createFailed": "Sovelluksen salasanan luonti epäonnistui", + "edit": "Muokkaa", + "updated": "Sovelluksen salasana päivitetty", + "updateFailed": "Sovelluksen salasanan päivitys epäonnistui", "deleteFailed": "Sovelluksen salasanan peruutus epäonnistui", "saveWarning": "Tallenna tämä salasana nyt - et näe sitä enää" }, diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index a0c8047..30f77b5 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -298,6 +298,9 @@ "deleted": "Mot de passe d'application révoqué", "loadFailed": "Échec du chargement des mots de passe d'application", "createFailed": "Échec de la création du mot de passe d'application", + "edit": "Modifier", + "updated": "Mot de passe d'application mis à jour", + "updateFailed": "Échec de la mise à jour du mot de passe d'application", "deleteFailed": "Échec de la révocation du mot de passe d'application", "saveWarning": "Enregistrez ce mot de passe maintenant — vous ne le verrez plus", "saveWarningTitle": "Important : enregistrez ce mot de passe d'application !", diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index 785a679..15524a9 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -310,6 +310,9 @@ "deleted": "アプリパスワードを取り消しました", "loadFailed": "アプリパスワードの読み込みに失敗しました", "createFailed": "アプリパスワードの作成に失敗しました", + "edit": "編集", + "updated": "アプリパスワードを更新しました", + "updateFailed": "アプリパスワードの更新に失敗しました", "deleteFailed": "アプリパスワードの取り消しに失敗しました", "saveWarning": "このパスワードを今すぐ保存してください - 再表示できません" }, diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 588d009..8df3b9f 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -310,6 +310,9 @@ "deleted": "앱 비밀번호가 취소되었습니다", "loadFailed": "앱 비밀번호 로딩 실패", "createFailed": "앱 비밀번호 생성 실패", + "edit": "편집", + "updated": "앱 비밀번호가 업데이트되었습니다", + "updateFailed": "앱 비밀번호 업데이트 실패", "deleteFailed": "앱 비밀번호 취소 실패", "saveWarning": "지금 이 비밀번호를 저장하세요 - 다시 볼 수 없습니다" }, diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 1d79416..1deb862 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -310,6 +310,9 @@ "deleted": "Applösenord återkallat", "loadFailed": "Kunde inte ladda applösenord", "createFailed": "Kunde inte skapa applösenord", + "edit": "Redigera", + "updated": "Applösenord uppdaterat", + "updateFailed": "Kunde inte uppdatera applösenord", "deleteFailed": "Kunde inte återkalla applösenord", "saveWarning": "Spara detta lösenord nu - du kommer inte se det igen" }, diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 8fcc4ec..3c98efd 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -310,6 +310,9 @@ "deleted": "应用专用密码已撤销", "loadFailed": "加载应用专用密码失败", "createFailed": "创建应用专用密码失败", + "edit": "编辑", + "updated": "应用专用密码已更新", + "updateFailed": "更新应用专用密码失败", "deleteFailed": "撤销应用专用密码失败", "saveWarning": "请立即保存此密码 - 您将无法再次查看" }, diff --git a/frontend/src/routes/OAuthConsent.svelte b/frontend/src/routes/OAuthConsent.svelte index 02f619b..4ccb9b8 100644 --- a/frontend/src/routes/OAuthConsent.svelte +++ b/frontend/src/routes/OAuthConsent.svelte @@ -1,6 +1,7 @@