diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 71d7f59..ac4529f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -57,8 +57,9 @@ async function xrpc(method: string, options?: { message: res.statusText, })); if ( - res.status === 401 && err.error === "AuthenticationFailed" && token && - tokenRefreshCallback && !skipRetry + res.status === 401 && + (err.error === "AuthenticationFailed" || err.error === "ExpiredToken") && + token && tokenRefreshCallback && !skipRetry ) { const newToken = await tokenRefreshCallback(); if (newToken && newToken !== token) { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 329f990..8827011 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord, Telegram, Signal channels", "navRepo": "Repository Explorer", "navRepoDesc": "Browse and manage raw AT Protocol records", + "navDelegation": "Delegation", + "navDelegationDesc": "Manage account controllers and delegated accounts", "navAdmin": "Admin Panel", "navAdminDesc": "Server stats and admin operations" }, diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json index 081dec6..6fdebad 100644 --- a/frontend/src/locales/fi.json +++ b/frontend/src/locales/fi.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord-, Telegram-, Signal-kanavat", "navRepo": "Tietovarastoselaaja", "navRepoDesc": "Selaa ja hallitse raakoja AT Protocol -tietueita", + "navDelegation": "Delegointi", + "navDelegationDesc": "Hallitse tilin ohjaajia ja delegoituja tilejä", "navAdmin": "Ylläpitopaneeli", "navAdminDesc": "Palvelintilastot ja ylläpitotoiminnot" }, diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index 5ec666e..c2e7c6c 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord、Telegram、Signal チャンネル", "navRepo": "リポジトリエクスプローラー", "navRepoDesc": "AT Protocol レコードを閲覧・管理", + "navDelegation": "委任", + "navDelegationDesc": "アカウントコントローラーと委任アカウントを管理", "navAdmin": "管理パネル", "navAdminDesc": "サーバー統計と管理操作" }, diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index 59fce81..dc60d5e 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord, Telegram, Signal 채널", "navRepo": "저장소 탐색기", "navRepoDesc": "AT Protocol 레코드 탐색 및 관리", + "navDelegation": "위임", + "navDelegationDesc": "계정 컨트롤러 및 위임된 계정 관리", "navAdmin": "관리 패널", "navAdminDesc": "서버 통계 및 관리 작업" }, diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 4daaf9e..ea2bc07 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord, Telegram, Signal-kanaler", "navRepo": "Dataförvarsutforskare", "navRepoDesc": "Bläddra och hantera råa AT Protocol-poster", + "navDelegation": "Delegering", + "navDelegationDesc": "Hantera kontokontrollanter och delegerade konton", "navAdmin": "Adminpanel", "navAdminDesc": "Serverstatistik och administratörsoperationer" }, diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 936f6c1..4fcca43 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -172,6 +172,8 @@ "navCommsDesc": "Discord、Telegram、Signal 渠道设置", "navRepo": "数据浏览器", "navRepoDesc": "浏览和管理原始 AT Protocol 记录", + "navDelegation": "账户委托", + "navDelegationDesc": "管理控制者和委托账户", "navAdmin": "管理后台", "navAdminDesc": "服务器统计和管理操作" }, @@ -912,6 +914,7 @@ "actor": "执行者", "controller": "控制者", "account": "账户", + "accountCreated": "已创建委托账户:{handle}", "details": "详情", "actionGrantCreated": "授权创建", "actionGrantRevoked": "授权撤销", diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte index 24c173d..e272f54 100644 --- a/frontend/src/routes/Dashboard.svelte +++ b/frontend/src/routes/Dashboard.svelte @@ -187,8 +187,8 @@

{$_('dashboard.navRepoDesc')}

-

Delegation

-

Manage account controllers and delegated accounts

+

{$_('dashboard.navDelegation')}

+

{$_('dashboard.navDelegationDesc')}

{#if auth.session.isAdmin} diff --git a/src/api/server/verify_email.rs b/src/api/server/verify_email.rs index 438a279..5ec32c6 100644 --- a/src/api/server/verify_email.rs +++ b/src/api/server/verify_email.rs @@ -28,7 +28,7 @@ pub async fn verify_migration_email( identifier: input.email, }; - let result = super::verify_token::verify_token_internal(&state, None, token_input).await?; + let result = super::verify_token::verify_token_internal(&state, token_input).await?; Ok(Json(VerifyMigrationEmailOutput { success: result.success, diff --git a/src/api/server/verify_token.rs b/src/api/server/verify_token.rs index 60509c4..d36e629 100644 --- a/src/api/server/verify_token.rs +++ b/src/api/server/verify_token.rs @@ -1,7 +1,7 @@ use axum::{ Json, extract::State, - http::{HeaderMap, StatusCode}, + http::StatusCode, }; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -30,15 +30,13 @@ pub struct VerifyTokenOutput { pub async fn verify_token( State(state): State, - headers: HeaderMap, Json(input): Json, ) -> Result, (StatusCode, Json)> { - verify_token_internal(&state, Some(&headers), input).await + verify_token_internal(&state, input).await } pub async fn verify_token_internal( state: &AppState, - headers: Option<&HeaderMap>, input: VerifyTokenInput, ) -> Result, (StatusCode, Json)> { let normalized_token = normalize_token_input(&input.token); @@ -95,15 +93,6 @@ pub async fn verify_token_internal( .await } VerificationPurpose::ChannelUpdate => { - let auth_did = extract_and_validate_auth(state, headers).await?; - if auth_did != token_data.did { - return Err(( - StatusCode::BAD_REQUEST, - Json( - json!({ "error": "InvalidToken", "message": "Token does not match authenticated account" }), - ), - )); - } handle_channel_update(state, &token_data.did, &token_data.channel, &identifier).await } VerificationPurpose::Signup => { @@ -113,39 +102,6 @@ pub async fn verify_token_internal( } } -async fn extract_and_validate_auth( - state: &AppState, - headers: Option<&HeaderMap>, -) -> Result)> { - let headers = headers.ok_or_else(|| { - ( - StatusCode::UNAUTHORIZED, - Json(json!({ "error": "AuthenticationRequired", "message": "Authentication required for this verification" })), - ) - })?; - - let token = crate::auth::extract_bearer_token_from_header( - headers.get("Authorization").and_then(|h| h.to_str().ok()), - ) - .ok_or_else(|| { - ( - StatusCode::UNAUTHORIZED, - Json(json!({ "error": "AuthenticationRequired", "message": "Authentication required for this verification" })), - ) - })?; - - let user = crate::auth::validate_bearer_token(&state.db, &token) - .await - .map_err(|_| { - ( - StatusCode::UNAUTHORIZED, - Json(json!({ "error": "AuthenticationFailed", "message": "Invalid authentication token" })), - ) - })?; - - Ok(user.did) -} - async fn handle_migration_verification( state: &AppState, did: &str, diff --git a/src/api/verification.rs b/src/api/verification.rs index 3d7aceb..036ff6b 100644 --- a/src/api/verification.rs +++ b/src/api/verification.rs @@ -2,7 +2,6 @@ use crate::state::AppState; use axum::{ Json, extract::State, - http::HeaderMap, response::{IntoResponse, Response}, }; use serde::Deserialize; @@ -18,7 +17,6 @@ pub struct ConfirmChannelVerificationInput { pub async fn confirm_channel_verification( State(state): State, - headers: HeaderMap, Json(input): Json, ) -> Response { let token_input = crate::api::server::VerifyTokenInput { @@ -26,7 +24,7 @@ pub async fn confirm_channel_verification( identifier: input.identifier, }; - match crate::api::server::verify_token_internal(&state, Some(&headers), token_input).await { + match crate::api::server::verify_token_internal(&state, token_input).await { Ok(output) => Json(json!({"success": output.success})).into_response(), Err((status, err_json)) => (status, err_json).into_response(), } diff --git a/src/comms/locale.rs b/src/comms/locale.rs index 895d7e5..708f4ff 100644 --- a/src/comms/locale.rs +++ b/src/comms/locale.rs @@ -49,7 +49,7 @@ static STRINGS_EN: NotificationStrings = NotificationStrings { password_reset_subject: "Password Reset - {hostname}", password_reset_body: "Hello @{handle},\n\nYour password reset code is: {code}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this message.", email_update_subject: "Confirm your new email - {hostname}", - email_update_body: "Hello @{handle},\n\nYour verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this email.\n\n(Or if you like to live dangerously: {verify_link})", + email_update_body: "Hello @{handle},\n\nYour verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 10 minutes.\n\nOr if you like to live dangerously:\n{verify_link}\n\nIf you did not request this, please ignore this email.", account_deletion_subject: "Account Deletion Request - {hostname}", account_deletion_body: "Hello @{handle},\n\nYour account deletion confirmation code is: {code}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please secure your account immediately.", plc_operation_subject: "{hostname} - PLC Operation Token", @@ -59,11 +59,11 @@ static STRINGS_EN: NotificationStrings = NotificationStrings { passkey_recovery_subject: "Account Recovery - {hostname}", passkey_recovery_body: "Hello @{handle},\n\nYou requested to recover your passkey-only account.\n\nClick the link below to set a temporary password and regain access:\n{url}\n\nThis link will expire in 1 hour.\n\nIf you did not request this, please ignore this message. Your account remains secure.", signup_verification_subject: "Verify your account - {hostname}", - signup_verification_body: "Welcome! Your verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 30 minutes.\n\nIf you did not create an account on {hostname}, please ignore this message.\n\n(Or if you like to live dangerously: {verify_link})", + signup_verification_body: "Welcome! Your verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 30 minutes.\n\nOr if you like to live dangerously:\n{verify_link}\n\nIf you did not create an account on {hostname}, please ignore this message.", legacy_login_subject: "Security Alert: Legacy Login Detected - {hostname}", legacy_login_body: "Hello @{handle},\n\nA login to your account was detected using a legacy app (like Bluesky) that doesn't support TOTP verification.\n\nDetails:\n- Time: {timestamp}\n- IP Address: {ip}\n\nYour TOTP protection was bypassed for this login. The session has limited permissions for sensitive operations.\n\nIf this wasn't you, please:\n1. Change your password immediately\n2. Review your active sessions\n3. Consider disabling legacy app logins in your security settings\n\nStay safe,\n{hostname}", migration_verification_subject: "Verify your email - {hostname}", - migration_verification_body: "Welcome to {hostname}!\n\nYour account has been migrated successfully. To complete the setup, please verify your email address.\n\nYour verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 48 hours.\n\nIf you did not migrate your account, please ignore this email.\n\n(Or if you like to live dangerously: {verify_link})", + migration_verification_body: "Welcome to {hostname}!\n\nYour account has been migrated successfully. To complete the setup, please verify your email address.\n\nYour verification code is:\n{code}\n\nCopy the code above and enter it at:\n{verify_page}\n\nThis code will expire in 48 hours.\n\nOr if you like to live dangerously:\n{verify_link}\n\nIf you did not migrate your account, please ignore this email.", }; static STRINGS_ZH: NotificationStrings = NotificationStrings { @@ -72,7 +72,7 @@ static STRINGS_ZH: NotificationStrings = NotificationStrings { password_reset_subject: "密码重置 - {hostname}", password_reset_body: "您好 @{handle},\n\n您的密码重置验证码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此消息。", email_update_subject: "确认您的新邮箱 - {hostname}", - email_update_body: "您好 @{handle},\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此邮件。\n\n(或者直接点击链接:{verify_link})", + email_update_body: "您好 @{handle},\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在10分钟后过期。\n\n或者直接点击链接:\n{verify_link}\n\n如果这不是您的操作,请忽略此邮件。", account_deletion_subject: "账户删除请求 - {hostname}", account_deletion_body: "您好 @{handle},\n\n您的账户删除确认码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请立即保护您的账户。", plc_operation_subject: "{hostname} - PLC 操作令牌", @@ -82,11 +82,11 @@ static STRINGS_ZH: NotificationStrings = NotificationStrings { passkey_recovery_subject: "账户恢复 - {hostname}", passkey_recovery_body: "您好 @{handle},\n\n您请求恢复仅通行密钥账户的访问权限。\n\n点击以下链接设置临时密码并恢复访问:\n{url}\n\n此链接将在1小时后过期。\n\n如果这不是您的操作,请忽略此消息。您的账户仍然安全。", signup_verification_subject: "验证您的账户 - {hostname}", - signup_verification_body: "欢迎!您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在30分钟后过期。\n\n如果您没有在 {hostname} 上创建账户,请忽略此消息。\n\n(或者直接点击链接:{verify_link})", + signup_verification_body: "欢迎!您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在30分钟后过期。\n\n或者直接点击链接:\n{verify_link}\n\n如果您没有在 {hostname} 上创建账户,请忽略此消息。", legacy_login_subject: "安全提醒:检测到传统应用登录 - {hostname}", legacy_login_body: "您好 @{handle},\n\n检测到使用不支持 TOTP 验证的传统应用(如 Bluesky)登录您的账户。\n\n详细信息:\n- 时间:{timestamp}\n- IP 地址:{ip}\n\n此次登录绕过了 TOTP 保护。该会话对敏感操作的权限有限。\n\n如果这不是您的操作,请:\n1. 立即更改密码\n2. 检查您的活跃会话\n3. 考虑在安全设置中禁用传统应用登录\n\n请注意安全,\n{hostname}", migration_verification_subject: "验证您的邮箱 - {hostname}", - migration_verification_body: "欢迎来到 {hostname}!\n\n您的账户已成功迁移。要完成设置,请验证您的邮箱地址。\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在 48 小时后过期。\n\n如果您没有迁移账户,请忽略此邮件。\n\n(或者直接点击链接:{verify_link})", + migration_verification_body: "欢迎来到 {hostname}!\n\n您的账户已成功迁移。要完成设置,请验证您的邮箱地址。\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在 48 小时后过期。\n\n或者直接点击链接:\n{verify_link}\n\n如果您没有迁移账户,请忽略此邮件。", }; static STRINGS_JA: NotificationStrings = NotificationStrings { @@ -95,7 +95,7 @@ static STRINGS_JA: NotificationStrings = NotificationStrings { password_reset_subject: "パスワードリセット - {hostname}", password_reset_body: "@{handle} 様\n\nパスワードリセットコードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメッセージを無視してください。", email_update_subject: "新しいメールアドレスの確認 - {hostname}", - email_update_body: "@{handle} 様\n\n確認コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})", + email_update_body: "@{handle} 様\n\n確認コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは10分後に期限切れとなります。\n\n自己責任でワンクリック認証:\n{verify_link}\n\nこの操作に心当たりがない場合は、このメールを無視してください。", account_deletion_subject: "アカウント削除リクエスト - {hostname}", account_deletion_body: "@{handle} 様\n\nアカウント削除の確認コードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、直ちにアカウントを保護してください。", plc_operation_subject: "{hostname} - PLC 操作トークン", @@ -105,11 +105,11 @@ static STRINGS_JA: NotificationStrings = NotificationStrings { passkey_recovery_subject: "アカウント復旧 - {hostname}", passkey_recovery_body: "@{handle} 様\n\nパスキー専用アカウントの復旧をリクエストされました。\n\n以下のリンクをクリックして一時パスワードを設定し、アクセスを回復してください:\n{url}\n\nこのリンクは1時間後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメッセージを無視してください。アカウントは安全なままです。", signup_verification_subject: "アカウント認証 - {hostname}", - signup_verification_body: "ようこそ!認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは30分後に期限切れとなります。\n\n{hostname} でアカウントを作成していない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})", + signup_verification_body: "ようこそ!認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは30分後に期限切れとなります。\n\n自己責任でワンクリック認証:\n{verify_link}\n\n{hostname} でアカウントを作成していない場合は、このメールを無視してください。", legacy_login_subject: "セキュリティ警告:レガシーログインを検出 - {hostname}", legacy_login_body: "@{handle} 様\n\nTOTP 認証に対応していないレガシーアプリ(Bluesky など)からのログインが検出されました。\n\n詳細:\n- 時刻:{timestamp}\n- IP アドレス:{ip}\n\nこのログインでは TOTP 保護がバイパスされました。このセッションは機密操作に対する権限が制限されています。\n\n心当たりがない場合は:\n1. 直ちにパスワードを変更してください\n2. アクティブなセッションを確認してください\n3. セキュリティ設定でレガシーアプリのログインを無効にすることを検討してください\n\nご注意ください。\n{hostname}", migration_verification_subject: "メールアドレスの認証 - {hostname}", - migration_verification_body: "{hostname} へようこそ!\n\nアカウントの移行が完了しました。設定を完了するには、メールアドレスを認証してください。\n\n認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは48時間後に期限切れとなります。\n\nアカウントを移行していない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})", + migration_verification_body: "{hostname} へようこそ!\n\nアカウントの移行が完了しました。設定を完了するには、メールアドレスを認証してください。\n\n認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは48時間後に期限切れとなります。\n\n自己責任でワンクリック認証:\n{verify_link}\n\nアカウントを移行していない場合は、このメールを無視してください。", }; static STRINGS_KO: NotificationStrings = NotificationStrings { @@ -118,7 +118,7 @@ static STRINGS_KO: NotificationStrings = NotificationStrings { password_reset_subject: "비밀번호 재설정 - {hostname}", password_reset_body: "안녕하세요 @{handle}님,\n\n비밀번호 재설정 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 메시지를 무시하세요.", email_update_subject: "새 이메일 주소 확인 - {hostname}", - email_update_body: "안녕하세요 @{handle}님,\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})", + email_update_body: "안녕하세요 @{handle}님,\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 10분 후에 만료됩니다.\n\n위험을 감수하고 원클릭 인증:\n{verify_link}\n\n요청하지 않으셨다면 이 이메일을 무시하세요.", account_deletion_subject: "계정 삭제 요청 - {hostname}", account_deletion_body: "안녕하세요 @{handle}님,\n\n계정 삭제 확인 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 즉시 계정을 보호하세요.", plc_operation_subject: "{hostname} - PLC 작업 토큰", @@ -128,11 +128,11 @@ static STRINGS_KO: NotificationStrings = NotificationStrings { passkey_recovery_subject: "계정 복구 - {hostname}", passkey_recovery_body: "안녕하세요 @{handle}님,\n\n패스키 전용 계정 복구를 요청하셨습니다.\n\n아래 링크를 클릭하여 임시 비밀번호를 설정하고 액세스를 복구하세요:\n{url}\n\n이 링크는 1시간 후에 만료됩니다.\n\n요청하지 않으셨다면 이 메시지를 무시하세요. 계정은 안전하게 유지됩니다.", signup_verification_subject: "계정 인증 - {hostname}", - signup_verification_body: "환영합니다! 인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 30분 후에 만료됩니다.\n\n{hostname}에서 계정을 만들지 않았다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})", + signup_verification_body: "환영합니다! 인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 30분 후에 만료됩니다.\n\n위험을 감수하고 원클릭 인증:\n{verify_link}\n\n{hostname}에서 계정을 만들지 않았다면 이 이메일을 무시하세요.", legacy_login_subject: "보안 알림: 레거시 로그인 감지 - {hostname}", legacy_login_body: "안녕하세요 @{handle}님,\n\nTOTP 인증을 지원하지 않는 레거시 앱(예: Bluesky)을 사용한 로그인이 감지되었습니다.\n\n세부 정보:\n- 시간: {timestamp}\n- IP 주소: {ip}\n\n이 로그인에서 TOTP 보호가 우회되었습니다. 이 세션은 민감한 작업에 대한 권한이 제한됩니다.\n\n본인이 아닌 경우:\n1. 즉시 비밀번호를 변경하세요\n2. 활성 세션을 검토하세요\n3. 보안 설정에서 레거시 앱 로그인 비활성화를 고려하세요\n\n{hostname} 드림", migration_verification_subject: "이메일 인증 - {hostname}", - migration_verification_body: "{hostname}에 오신 것을 환영합니다!\n\n계정 마이그레이션이 완료되었습니다. 설정을 완료하려면 이메일 주소를 인증하세요.\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 48시간 후에 만료됩니다.\n\n계정을 마이그레이션하지 않았다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})", + migration_verification_body: "{hostname}에 오신 것을 환영합니다!\n\n계정 마이그레이션이 완료되었습니다. 설정을 완료하려면 이메일 주소를 인증하세요.\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 48시간 후에 만료됩니다.\n\n위험을 감수하고 원클릭 인증:\n{verify_link}\n\n계정을 마이그레이션하지 않았다면 이 이메일을 무시하세요.", }; static STRINGS_SV: NotificationStrings = NotificationStrings { @@ -141,7 +141,7 @@ static STRINGS_SV: NotificationStrings = NotificationStrings { password_reset_subject: "Lösenordsåterställning - {hostname}", password_reset_body: "Hej @{handle},\n\nDin kod för lösenordsåterställning är: {code}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta kan du ignorera detta meddelande.", email_update_subject: "Bekräfta din nya e-post - {hostname}", - email_update_body: "Hej @{handle},\n\nDin verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta kan du ignorera detta meddelande.\n\n(Eller om du gillar att leva farligt: {verify_link})", + email_update_body: "Hej @{handle},\n\nDin verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 10 minuter.\n\nEller om du gillar att leva farligt:\n{verify_link}\n\nOm du inte begärde detta kan du ignorera detta meddelande.", account_deletion_subject: "Begäran om kontoradering - {hostname}", account_deletion_body: "Hej @{handle},\n\nDin bekräftelsekod för kontoradering är: {code}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta, skydda ditt konto omedelbart.", plc_operation_subject: "{hostname} - PLC-operationstoken", @@ -151,11 +151,11 @@ static STRINGS_SV: NotificationStrings = NotificationStrings { passkey_recovery_subject: "Kontoåterställning - {hostname}", passkey_recovery_body: "Hej @{handle},\n\nDu begärde att återställa ditt endast nyckelkonto.\n\nKlicka på länken nedan för att ställa in ett tillfälligt lösenord och återfå åtkomst:\n{url}\n\nDenna länk upphör om 1 timme.\n\nOm du inte begärde detta kan du ignorera detta meddelande. Ditt konto förblir säkert.", signup_verification_subject: "Verifiera ditt konto - {hostname}", - signup_verification_body: "Välkommen! Din verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 30 minuter.\n\nOm du inte skapade ett konto på {hostname}, ignorera detta meddelande.\n\n(Eller om du gillar att leva farligt: {verify_link})", + signup_verification_body: "Välkommen! Din verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 30 minuter.\n\nEller om du gillar att leva farligt:\n{verify_link}\n\nOm du inte skapade ett konto på {hostname}, ignorera detta meddelande.", legacy_login_subject: "Säkerhetsvarning: Äldre inloggning upptäckt - {hostname}", legacy_login_body: "Hej @{handle},\n\nEn inloggning till ditt konto upptäcktes med en äldre app (som Bluesky) som inte stöder TOTP-verifiering.\n\nDetaljer:\n- Tid: {timestamp}\n- IP-adress: {ip}\n\nDitt TOTP-skydd kringgicks för denna inloggning. Sessionen har begränsade behörigheter för känsliga operationer.\n\nOm detta inte var du:\n1. Ändra ditt lösenord omedelbart\n2. Granska dina aktiva sessioner\n3. Överväg att inaktivera äldre appinloggningar i dina säkerhetsinställningar\n\nVar försiktig,\n{hostname}", migration_verification_subject: "Verifiera din e-post - {hostname}", - migration_verification_body: "Välkommen till {hostname}!\n\nDitt konto har migrerats framgångsrikt. För att slutföra installationen, verifiera din e-postadress.\n\nDin verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 48 timmar.\n\nOm du inte migrerade ditt konto kan du ignorera detta meddelande.\n\n(Eller om du gillar att leva farligt: {verify_link})", + migration_verification_body: "Välkommen till {hostname}!\n\nDitt konto har migrerats framgångsrikt. För att slutföra installationen, verifiera din e-postadress.\n\nDin verifieringskod är:\n{code}\n\nKopiera koden ovan och ange den på:\n{verify_page}\n\nDenna kod upphör om 48 timmar.\n\nEller om du gillar att leva farligt:\n{verify_link}\n\nOm du inte migrerade ditt konto kan du ignorera detta meddelande.", }; static STRINGS_FI: NotificationStrings = NotificationStrings { @@ -164,7 +164,7 @@ static STRINGS_FI: NotificationStrings = NotificationStrings { password_reset_subject: "Salasanan palautus - {hostname}", password_reset_body: "Hei @{handle},\n\nSalasanan palautuskoodisi on: {code}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta.", email_update_subject: "Vahvista uusi sähköpostisi - {hostname}", - email_update_body: "Hei @{handle},\n\nVahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta.\n\n(Tai jos pidät vaarallisesta elämästä: {verify_link})", + email_update_body: "Hei @{handle},\n\nVahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 10 minuutissa.\n\nTai jos pidät vaarallisesta elämästä:\n{verify_link}\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta.", account_deletion_subject: "Tilin poistopyyntö - {hostname}", account_deletion_body: "Hei @{handle},\n\nTilin poiston vahvistuskoodisi on: {code}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, suojaa tilisi välittömästi.", plc_operation_subject: "{hostname} - PLC-toimintotunniste", @@ -174,11 +174,11 @@ static STRINGS_FI: NotificationStrings = NotificationStrings { passkey_recovery_subject: "Tilin palautus - {hostname}", passkey_recovery_body: "Hei @{handle},\n\nPyysit palauttamaan vain pääsyavaintilisi.\n\nKlikkaa alla olevaa linkkiä asettaaksesi väliaikaisen salasanan ja saadaksesi pääsyn takaisin:\n{url}\n\nTämä linkki vanhenee tunnissa.\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta. Tilisi pysyy turvassa.", signup_verification_subject: "Vahvista tilisi - {hostname}", - signup_verification_body: "Tervetuloa! Vahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 30 minuutissa.\n\nJos et luonut tiliä palveluun {hostname}, jätä tämä viesti huomiotta.\n\n(Tai jos pidät vaarallisesta elämästä: {verify_link})", + signup_verification_body: "Tervetuloa! Vahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 30 minuutissa.\n\nTai jos pidät vaarallisesta elämästä:\n{verify_link}\n\nJos et luonut tiliä palveluun {hostname}, jätä tämä viesti huomiotta.", legacy_login_subject: "Turvallisuushälytys: Vanha kirjautuminen havaittu - {hostname}", legacy_login_body: "Hei @{handle},\n\nTilillesi havaittiin kirjautuminen vanhalla sovelluksella (kuten Bluesky), joka ei tue TOTP-vahvistusta.\n\nTiedot:\n- Aika: {timestamp}\n- IP-osoite: {ip}\n\nTOTP-suojauksesi ohitettiin tässä kirjautumisessa. Istunnolla on rajoitetut oikeudet arkaluontoisiin toimintoihin.\n\nJos tämä et ollut sinä:\n1. Vaihda salasanasi välittömästi\n2. Tarkista aktiiviset istuntosi\n3. Harkitse vanhojen sovellusten kirjautumisen poistamista käytöstä turvallisuusasetuksissa\n\nOle varovainen,\n{hostname}", migration_verification_subject: "Vahvista sähköpostisi - {hostname}", - migration_verification_body: "Tervetuloa palveluun {hostname}!\n\nTilisi on siirretty onnistuneesti. Viimeistele asennus vahvistamalla sähköpostiosoitteesi.\n\nVahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 48 tunnissa.\n\nJos et siirtänyt tiliäsi, voit jättää tämän viestin huomiotta.\n\n(Tai jos pidät vaarallisesta elämästä: {verify_link})", + migration_verification_body: "Tervetuloa palveluun {hostname}!\n\nTilisi on siirretty onnistuneesti. Viimeistele asennus vahvistamalla sähköpostiosoitteesi.\n\nVahvistuskoodisi on:\n{code}\n\nKopioi koodi yllä ja syötä se osoitteessa:\n{verify_page}\n\nTämä koodi vanhenee 48 tunnissa.\n\nTai jos pidät vaarallisesta elämästä:\n{verify_link}\n\nJos et siirtänyt tiliäsi, voit jättää tämän viestin huomiotta.", }; pub fn format_message(template: &str, vars: &[(&str, &str)]) -> String { diff --git a/src/comms/sender.rs b/src/comms/sender.rs index 236b3db..a1dc67c 100644 --- a/src/comms/sender.rs +++ b/src/comms/sender.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use reqwest::Client; use serde_json::json; use std::process::Stdio; @@ -57,6 +58,15 @@ pub fn sanitize_header_value(value: &str) -> String { value.replace(['\r', '\n'], " ").trim().to_string() } +pub fn mime_encode_header(value: &str) -> String { + if value.is_ascii() { + sanitize_header_value(value) + } else { + let sanitized = sanitize_header_value(value); + format!("=?UTF-8?B?{}?=", BASE64.encode(sanitized.as_bytes())) + } +} + pub fn is_valid_phone_number(number: &str) -> bool { if number.len() < 2 || number.len() > 20 { return false; @@ -94,7 +104,7 @@ impl EmailSender { pub fn format_email(&self, notification: &QueuedComms) -> String { let subject = - sanitize_header_value(notification.subject.as_deref().unwrap_or("Notification")); + mime_encode_header(notification.subject.as_deref().unwrap_or("Notification")); let recipient = sanitize_header_value(¬ification.recipient); let from_header = if self.from_name.is_empty() { self.from_address.clone()