···164164 "changeEmailButton": "Change Email",
165165 "requesting": "Requesting...",
166166 "verificationCode": "Verification Code",
167167- "verificationCodePlaceholder": "Enter code from email",
167167+ "verificationCodePlaceholder": "Enter verification code",
168168 "confirmEmailChange": "Confirm Email Change",
169169 "updating": "Updating...",
170170 "changeHandle": "Change Handle",
···202202 "deleteAccount": "Delete Account",
203203 "deleteWarning": "This action is irreversible. All your data will be permanently deleted.",
204204 "requestDeletion": "Request Account Deletion",
205205- "confirmationCode": "Confirmation Code (from email)",
205205+ "confirmationCode": "Confirmation Code",
206206 "confirmationCodePlaceholder": "Enter confirmation code",
207207 "yourPassword": "Your Password",
208208 "yourPasswordPlaceholder": "Enter your password",
209209 "permanentlyDelete": "Permanently Delete Account",
210210 "deleting": "Deleting...",
211211 "messages": {
212212- "emailCodeSent": "Verification code sent to your current email",
212212+ "emailCodeSent": "Verification code sent to your notification channel",
213213 "emailUpdated": "Email updated successfully",
214214 "handleUpdated": "Handle updated successfully",
215215 "passwordChanged": "Password changed successfully",
···451451 },
452452 "admin": {
453453 "title": "Admin Panel",
454454+ "loading": "Loading...",
455455+ "serverConfig": "Server Configuration",
456456+ "serverName": "Server Name",
457457+ "serverNamePlaceholder": "My PDS",
458458+ "serverNameHelp": "Displayed in the browser tab and other places",
459459+ "serverLogo": "Server Logo",
460460+ "logoPreview": "Logo preview",
461461+ "removeLogo": "Remove",
462462+ "logoHelp": "Used as favicon and shown in the navbar",
463463+ "themeColors": "Theme Colors",
464464+ "themeColorsHint": "Leave blank to use default colors.",
465465+ "primaryLight": "Primary (Light Mode)",
466466+ "primaryLightDefault": "#2c00ff (default)",
467467+ "primaryDark": "Primary (Dark Mode)",
468468+ "primaryDarkDefault": "#7b6bff (default)",
469469+ "secondaryLight": "Secondary (Light Mode)",
470470+ "secondaryLightDefault": "#ff2400 (default)",
471471+ "secondaryDark": "Secondary (Dark Mode)",
472472+ "secondaryDarkDefault": "#ff6b5b (default)",
473473+ "configSaved": "Server configuration saved",
474474+ "saving": "Saving...",
475475+ "saveConfig": "Save Configuration",
454476 "serverStats": "Server Statistics",
455477 "users": "Users",
456478 "repos": "Repositories",
···580602 "verify": {
581603 "title": "Verify Your Account",
582604 "subtitle": "We've sent a verification code to your {channel}. Enter it below to complete registration.",
583583- "codePlaceholder": "Enter 6-digit code",
605605+ "tokenSubtitle": "Enter the verification code and the identifier it was sent to.",
606606+ "tokenTitle": "Verify",
607607+ "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
584608 "codeLabel": "Verification Code",
609609+ "codeHelp": "Copy the entire code from your message, including dashes",
585610 "verifyButton": "Verify Account",
611611+ "verify": "Verify",
586612 "verifying": "Verifying...",
613613+ "pleaseWait": "Please wait...",
587614 "resendCode": "Resend Code",
588615 "resending": "Resending...",
616616+ "sending": "Sending...",
589617 "codeResent": "Verification code resent!",
618618+ "codeResentDetail": "Verification code sent! Check your inbox.",
590619 "backToLogin": "Back to Login",
591620 "verifyingAccount": "Verifying account: @{handle}",
592621 "startOver": "Start over with a different account",
593622 "noPending": "No pending verification found.",
594623 "noPendingInfo": "If you recently created an account and need to verify it, you may need to create a new account. If you already verified your account, you can sign in.",
595624 "createAccount": "Create Account",
596596- "signIn": "Sign In"
625625+ "signIn": "Sign In",
626626+ "verified": "Verified!",
627627+ "channelVerified": "Your {channel} has been verified successfully.",
628628+ "canNowSignIn": "You can now sign in to your account.",
629629+ "continue": "Continue",
630630+ "identifierLabel": "Email or Identifier",
631631+ "identifierPlaceholder": "you@example.com",
632632+ "identifierHelp": "The email address or identifier the code was sent to"
597633 },
598634 "resetPassword": {
599635 "title": "Reset Password",
···605641 "sendCode": "Send Reset Code",
606642 "sending": "Sending...",
607643 "codeSent": "Password reset code sent! Check your preferred notification channel.",
608608- "enterCode": "Enter the code from your email and your new password.",
644644+ "enterCode": "Enter the code you received and your new password.",
609645 "code": "Reset Code",
610646 "codePlaceholder": "Enter reset code",
611647 "newPassword": "New Password",
···664700 },
665701 "registerPasskey": {
666702 "title": "Create Passkey Account",
667667- "subtitle": "Create a passwordless account using a passkey.",
703703+ "subtitle": "Create an ultra-secure account using a passkey instead of a password.",
704704+ "subtitleKeyChoice": "Choose how to set up your external did:web identity.",
705705+ "subtitleInitialDidDoc": "Upload your DID document to continue.",
706706+ "subtitleCreating": "Creating your account...",
707707+ "subtitlePasskey": "Register your passkey to secure your account.",
708708+ "subtitleAppPassword": "Save your app password for third-party apps.",
709709+ "subtitleVerify": "Verify your {channel} to continue.",
710710+ "subtitleUpdatedDidDoc": "Update your DID document with the PDS signing key.",
711711+ "subtitleActivating": "Activating your account...",
712712+ "subtitleComplete": "Your account has been created successfully!",
668713 "handle": "Handle",
669714 "handlePlaceholder": "yourname",
670715 "handleHint": "Your full handle will be: @{handle}",
716716+ "handleDotWarning": "Custom domain handles can be set up after account creation.",
671717 "email": "Email Address",
672718 "emailPlaceholder": "you@example.com",
673719 "inviteCode": "Invite Code",
674720 "inviteCodePlaceholder": "Enter your invite code",
675721 "createButton": "Create Account",
676722 "creating": "Creating...",
723723+ "continue": "Continue",
724724+ "back": "Back",
677725 "alreadyHaveAccount": "Already have an account?",
678726 "signIn": "Sign in",
679727 "wantPassword": "Want to use a password?",
680680- "createPasswordAccount": "Create a password account"
728728+ "createPasswordAccount": "Create a password account",
729729+ "wantTraditional": "Want a traditional password?",
730730+ "registerWithPassword": "Register with password",
731731+ "contactMethod": "Contact Method",
732732+ "contactMethodHint": "Choose how you'd like to verify your account and receive notifications.",
733733+ "verificationMethod": "Verification Method",
734734+ "identityType": "Identity Type",
735735+ "identityTypeHint": "Choose how your decentralized identity will be managed.",
736736+ "didPlcRecommended": "did:plc (Recommended)",
737737+ "didPlcHint": "Portable identity managed by PLC Directory",
738738+ "didWeb": "did:web",
739739+ "didWebHint": "Identity hosted on this PDS (read warning below)",
740740+ "didWebBYOD": "did:web (BYOD)",
741741+ "didWebBYODHint": "Bring your own domain",
742742+ "didWebWarningTitle": "Important: Understand the trade-offs",
743743+ "didWebWarning1": "Permanent tie to this PDS:",
744744+ "didWebWarning2": "No recovery mechanism:",
745745+ "didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys.",
746746+ "didWebWarning3": "We commit to you:",
747747+ "didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document.",
748748+ "didWebWarning4": "Recommendation:",
749749+ "didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.",
750750+ "externalDid": "Your did:web",
751751+ "externalDidPlaceholder": "did:web:yourdomain.com",
752752+ "externalDidHint": "You'll need to serve a DID document at",
753753+ "whyPasskeyOnly": "Why passkey-only?",
754754+ "whyPasskeyOnlyDesc": "Passkey accounts are more secure than password-based accounts because they:",
755755+ "whyPasskeyBullet1": "Cannot be phished or stolen in data breaches",
756756+ "whyPasskeyBullet2": "Use hardware-backed cryptographic keys",
757757+ "whyPasskeyBullet3": "Require your biometric or device PIN to use",
758758+ "passkeyNameLabel": "Passkey Name (optional)",
759759+ "passkeyNamePlaceholder": "e.g., MacBook Touch ID",
760760+ "passkeyNameHint": "A friendly name to identify this passkey",
761761+ "passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:",
762762+ "passkeyPromptBullet1": "Touch ID or Face ID",
763763+ "passkeyPromptBullet2": "Your device PIN or password",
764764+ "passkeyPromptBullet3": "A security key (if you have one)",
765765+ "createPasskey": "Create Passkey",
766766+ "creatingPasskey": "Creating Passkey...",
767767+ "redirecting": "Redirecting to dashboard...",
768768+ "loading": "Loading...",
769769+ "errors": {
770770+ "handleRequired": "Handle is required",
771771+ "handleNoDots": "Handle cannot contain dots. You can set up a custom domain handle after creating your account.",
772772+ "inviteRequired": "Invite code is required",
773773+ "externalDidRequired": "External did:web is required",
774774+ "externalDidFormat": "External DID must start with did:web:",
775775+ "emailRequired": "Email is required for email verification",
776776+ "discordRequired": "Discord ID is required for Discord verification",
777777+ "telegramRequired": "Telegram username is required for Telegram verification",
778778+ "signalRequired": "Phone number is required for Signal verification",
779779+ "passkeysNotSupported": "Passkeys are not supported in this browser. Please use a different browser or register with a password instead.",
780780+ "passkeyCancelled": "Passkey creation was cancelled",
781781+ "passkeyFailed": "Passkey registration failed"
782782+ }
681783 },
682784 "trustedDevices": {
683785 "title": "Trusted Devices",
···710812 "verify": "Verify",
711813 "verifying": "Verifying...",
712814 "cancel": "Cancel"
815815+ },
816816+ "verifyChannel": {
817817+ "title": "Verify Channel",
818818+ "subtitle": "Enter the verification code sent to your notification channel.",
819819+ "signInRequired": "Sign In Required",
820820+ "signInRequiredDesc": "You must be signed in to verify a channel.",
821821+ "signIn": "Sign In",
822822+ "verifying": "Verifying...",
823823+ "pleaseWait": "Please wait while we verify your channel.",
824824+ "successTitle": "Verified!",
825825+ "successDesc": "Your {channel} has been verified successfully.",
826826+ "backToSettings": "Back to Settings",
827827+ "channelLabel": "Channel",
828828+ "selectChannel": "Select channel...",
829829+ "identifierLabel": "Identifier",
830830+ "identifierPlaceholder": "Email, Discord ID, etc.",
831831+ "identifierHelp": "The email address, Discord ID, Telegram username, or Signal number being verified.",
832832+ "codeLabel": "Verification Code",
833833+ "codeHelp": "Copy the entire code from your message, including dashes.",
834834+ "verifyButton": "Verify"
713835 }
714836}
···6565 "didPlcHint": "PLC 디렉토리에서 관리하는 이동 가능한 ID",
6666 "didWeb": "did:web",
6767 "didWebHint": "이 PDS에서 호스팅되는 ID (아래 경고 참조)",
6868- "didWebBYOD": "did:web (BYOD)",
6868+ "didWebBYOD": "did:web (자체 도메인)",
6969 "didWebBYODHint": "자체 도메인 사용",
7070 "didWebWarningTitle": "중요: 장단점을 이해하세요",
7171 "didWebWarning1": "이 PDS에 영구 연결:",
···164164 "changeEmailButton": "이메일 변경",
165165 "requesting": "요청 중...",
166166 "verificationCode": "인증 코드",
167167- "verificationCodePlaceholder": "이메일의 코드 입력",
167167+ "verificationCodePlaceholder": "인증 코드 입력",
168168 "confirmEmailChange": "이메일 변경 확인",
169169 "updating": "업데이트 중...",
170170 "changeHandle": "핸들 변경",
···202202 "deleteAccount": "계정 삭제",
203203 "deleteWarning": "이 작업은 되돌릴 수 없습니다. 모든 데이터가 영구적으로 삭제됩니다.",
204204 "requestDeletion": "계정 삭제 요청",
205205- "confirmationCode": "확인 코드 (이메일에서)",
205205+ "confirmationCode": "확인 코드",
206206 "confirmationCodePlaceholder": "확인 코드 입력",
207207 "yourPassword": "비밀번호",
208208 "yourPasswordPlaceholder": "비밀번호 입력",
209209 "permanentlyDelete": "계정 영구 삭제",
210210 "deleting": "삭제 중...",
211211 "messages": {
212212- "emailCodeSent": "현재 이메일로 인증 코드를 보냈습니다",
212212+ "emailCodeSent": "알림 채널로 인증 코드를 보냈습니다",
213213 "emailUpdated": "이메일이 업데이트되었습니다",
214214 "handleUpdated": "핸들이 업데이트되었습니다",
215215 "passwordChanged": "비밀번호가 변경되었습니다",
···451451 },
452452 "admin": {
453453 "title": "관리 패널",
454454+ "loading": "로딩 중...",
455455+ "serverConfig": "서버 설정",
456456+ "serverName": "서버 이름",
457457+ "serverNamePlaceholder": "내 PDS",
458458+ "serverNameHelp": "브라우저 탭 및 다른 곳에 표시됩니다",
459459+ "serverLogo": "서버 로고",
460460+ "logoPreview": "로고 미리보기",
461461+ "removeLogo": "삭제",
462462+ "logoHelp": "파비콘으로 사용되며 네비게이션 바에 표시됩니다",
463463+ "themeColors": "테마 색상",
464464+ "themeColorsHint": "기본 색상을 사용하려면 비워 두세요.",
465465+ "primaryLight": "기본 (라이트 모드)",
466466+ "primaryDark": "기본 (다크 모드)",
467467+ "accentLight": "강조 (라이트 모드)",
468468+ "accentDark": "강조 (다크 모드)",
469469+ "faviconExample": "파비콘 예시",
470470+ "configSaved": "서버 설정이 저장되었습니다",
471471+ "saving": "저장 중...",
472472+ "saveConfig": "설정 저장",
454473 "serverStats": "서버 통계",
455474 "users": "사용자",
456475 "repos": "저장소",
···580599 "verify": {
581600 "title": "계정 인증",
582601 "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 입력하여 등록을 완료하세요.",
583583- "codePlaceholder": "6자리 코드 입력",
602602+ "tokenTitle": "인증",
603603+ "tokenSubtitle": "인증 코드와 전송된 식별자를 입력하세요.",
604604+ "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
584605 "codeLabel": "인증 코드",
606606+ "codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요",
585607 "verifyButton": "계정 인증",
608608+ "verify": "인증",
586609 "verifying": "인증 중...",
610610+ "pleaseWait": "잠시 기다려 주세요...",
611611+ "sending": "전송 중...",
587612 "resendCode": "코드 다시 보내기",
588613 "resending": "전송 중...",
589614 "codeResent": "인증 코드를 다시 보냈습니다!",
615615+ "codeResentDetail": "인증 코드가 전송되었습니다! 받은 편지함을 확인하세요.",
616616+ "verified": "인증 완료!",
617617+ "channelVerified": "{channel}이(가) 성공적으로 인증되었습니다.",
618618+ "canNowSignIn": "이제 계정에 로그인할 수 있습니다.",
619619+ "continue": "계속",
620620+ "identifierLabel": "이메일 또는 식별자",
621621+ "identifierPlaceholder": "you@example.com",
622622+ "identifierHelp": "코드가 전송된 이메일 주소 또는 식별자",
590623 "backToLogin": "로그인으로 돌아가기",
591624 "verifyingAccount": "인증 중인 계정: @{handle}",
592625 "startOver": "다른 계정으로 다시 시작",
···605638 "sendCode": "재설정 코드 보내기",
606639 "sending": "전송 중...",
607640 "codeSent": "비밀번호 재설정 코드를 보냈습니다! 선호하는 알림 채널을 확인하세요.",
608608- "enterCode": "이메일의 코드와 새 비밀번호를 입력하세요.",
641641+ "enterCode": "받은 코드와 새 비밀번호를 입력하세요.",
609642 "code": "재설정 코드",
610643 "codePlaceholder": "재설정 코드 입력",
611644 "newPassword": "새 비밀번호",
···664697 },
665698 "registerPasskey": {
666699 "title": "패스키 계정 만들기",
667667- "subtitle": "패스키를 사용하여 비밀번호 없는 계정을 만듭니다.",
700700+ "subtitle": "비밀번호 대신 패스키를 사용하여 초안전 계정을 만듭니다.",
701701+ "subtitleKeyChoice": "외부 did:web 아이덴티티 설정 방법을 선택하세요.",
702702+ "subtitleVerify": "{channel}(으)로 인증 코드를 보냈습니다. 코드를 입력하여 계속하세요.",
703703+ "subtitlePasskey": "패스키를 만들어 계정 설정을 완료하세요.",
668704 "handle": "핸들",
669705 "handlePlaceholder": "사용자 이름",
670706 "handleHint": "전체 핸들: @{handle}",
707707+ "contactMethod": "연락 방법",
708708+ "contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요.",
709709+ "verificationMethod": "인증 방법",
671710 "email": "이메일 주소",
672711 "emailPlaceholder": "you@example.com",
712712+ "discord": "Discord",
713713+ "discordId": "Discord 사용자 ID",
714714+ "discordIdPlaceholder": "Discord 사용자 ID",
715715+ "discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
716716+ "telegram": "Telegram",
717717+ "telegramUsername": "Telegram 사용자 이름",
718718+ "telegramUsernamePlaceholder": "@yourusername",
719719+ "signal": "Signal",
720720+ "signalNumber": "Signal 전화번호",
721721+ "signalNumberPlaceholder": "+821012345678",
722722+ "signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
673723 "inviteCode": "초대 코드",
674724 "inviteCodePlaceholder": "초대 코드 입력",
725725+ "inviteCodeRequired": "필수",
726726+ "didWebDescription": "자체 도메인에서 호스팅되는 DID 아이덴티티를 사용합니다.",
727727+ "didWebToggle": "외부 did:web 사용",
728728+ "externalDid": "귀하의 did:web",
729729+ "externalDidPlaceholder": "did:web:yourdomain.com",
730730+ "dnsVerificationInstructions": "도메인을 인증하려면 이 TXT 레코드를 추가하세요:",
731731+ "copyDid": "DID 복사",
675732 "createButton": "계정 만들기",
676733 "creating": "생성 중...",
677734 "alreadyHaveAccount": "이미 계정이 있으신가요?",
678735 "signIn": "로그인",
679736 "wantPassword": "비밀번호를 사용하시겠습니까?",
680680- "createPasswordAccount": "비밀번호 계정 만들기"
737737+ "createPasswordAccount": "비밀번호 계정 만들기",
738738+ "errors": {
739739+ "handleRequired": "핸들은 필수입니다",
740740+ "handleNoDots": "핸들에 점을 포함할 수 없습니다. 계정 생성 후 사용자 정의 도메인을 설정할 수 있습니다.",
741741+ "passkeysNotSupported": "이 브라우저에서 패스키가 지원되지 않습니다. 비밀번호 기반 계정을 만들거나 패스키를 지원하는 브라우저를 사용하세요.",
742742+ "passkeyCancelled": "패스키 생성이 취소되었습니다",
743743+ "passkeyFailed": "패스키 등록에 실패했습니다"
744744+ }
681745 },
682746 "trustedDevices": {
683747 "title": "신뢰할 수 있는 기기",
···710774 "verify": "확인",
711775 "verifying": "확인 중...",
712776 "cancel": "취소"
777777+ },
778778+ "verifyChannel": {
779779+ "title": "채널 인증",
780780+ "subtitle": "알림 채널로 전송된 인증 코드를 입력하세요.",
781781+ "signInRequired": "로그인 필요",
782782+ "signInRequiredDesc": "채널을 인증하려면 로그인해야 합니다.",
783783+ "signIn": "로그인",
784784+ "verifying": "인증 중...",
785785+ "pleaseWait": "채널을 인증하는 중입니다. 잠시 기다려 주세요.",
786786+ "successTitle": "인증 완료!",
787787+ "successDesc": "{channel}이(가) 성공적으로 인증되었습니다.",
788788+ "backToSettings": "설정으로 돌아가기",
789789+ "channelLabel": "채널",
790790+ "selectChannel": "채널 선택...",
791791+ "identifierLabel": "식별자",
792792+ "identifierPlaceholder": "이메일, Discord ID 등",
793793+ "identifierHelp": "인증할 이메일 주소, Discord ID, Telegram 사용자 이름 또는 Signal 번호.",
794794+ "codeLabel": "인증 코드",
795795+ "codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요.",
796796+ "verifyButton": "인증"
713797 }
714798}
+98-14
frontend/src/locales/sv.json
···8080 "externalDidPlaceholder": "did:web:dindomän.se",
8181 "externalDidHint": "Din domän måste tillhandahålla ett giltigt DID-dokument på /.well-known/did.json som pekar på denna PDS",
8282 "contactMethod": "Kontaktmetod",
8383- "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot notiser. Du behöver bara en.",
8383+ "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden. Du behöver bara en.",
8484 "verificationMethod": "Verifieringsmetod",
8585 "email": "E-post",
8686 "emailAddress": "E-postadress",
···164164 "changeEmailButton": "Ändra e-post",
165165 "requesting": "Begär...",
166166 "verificationCode": "Verifieringskod",
167167- "verificationCodePlaceholder": "Ange kod från e-post",
167167+ "verificationCodePlaceholder": "Ange verifieringskod",
168168 "confirmEmailChange": "Bekräfta e-poständring",
169169 "updating": "Uppdaterar...",
170170 "changeHandle": "Ändra användarnamn",
···202202 "deleteAccount": "Radera konto",
203203 "deleteWarning": "Denna åtgärd är oåterkallelig. All din data kommer att raderas permanent.",
204204 "requestDeletion": "Begär kontoradering",
205205- "confirmationCode": "Bekräftelsekod (från e-post)",
205205+ "confirmationCode": "Bekräftelsekod",
206206 "confirmationCodePlaceholder": "Ange bekräftelsekod",
207207 "yourPassword": "Ditt lösenord",
208208 "yourPasswordPlaceholder": "Ange ditt lösenord",
209209 "permanentlyDelete": "Radera konto permanent",
210210 "deleting": "Raderar...",
211211 "messages": {
212212- "emailCodeSent": "Verifieringskod skickad till din nuvarande e-post",
212212+ "emailCodeSent": "Verifieringskod skickad till din meddelandekanal",
213213 "emailUpdated": "E-post uppdaterad",
214214 "handleUpdated": "Användarnamn uppdaterat",
215215 "passwordChanged": "Lösenord ändrat",
···350350 "lastUsed": "Senast använd",
351351 "passwordDescription": "Hantera ditt kontolösenord. Om du har nycklar konfigurerade kan du valfritt ta bort ditt lösenord för en helt lösenordsfri upplevelse.",
352352 "disableTotpWarning": "Detta gör ditt konto mindre säkert.",
353353- "removePasswordWarning": "Detta gör ditt konto till endast nyckelkonto. Du kan endast logga in med dina registrerade nycklar. Om du förlorar tillgång till alla dina nycklar kan du återställa ditt konto via din notifieringskanal.",
353353+ "removePasswordWarning": "Detta gör ditt konto till endast nyckelkonto. Du kan endast logga in med dina registrerade nycklar. Om du förlorar tillgång till alla dina nycklar kan du återställa ditt konto via din meddelandekanal.",
354354 "beforeProceeding": "Innan du fortsätter:",
355355 "beforeProceedingItem1": "Se till att du har minst en pålitlig nyckel registrerad",
356356 "beforeProceedingItem2": "Överväg att registrera nycklar på flera enheter",
357357- "beforeProceedingItem3": "Se till att din återställningsnotifieringskanal är uppdaterad",
357357+ "beforeProceedingItem3": "Se till att din meddelandekanal för återställning är uppdaterad",
358358 "addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.",
359359 "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.",
360360 "trustedDevices": "Betrodda enheter",
···451451 },
452452 "admin": {
453453 "title": "Adminpanel",
454454+ "loading": "Laddar...",
455455+ "serverConfig": "Serverkonfiguration",
456456+ "serverName": "Servernamn",
457457+ "serverNamePlaceholder": "Min PDS",
458458+ "serverNameHelp": "Visas i webbläsarfliken och på andra ställen",
459459+ "serverLogo": "Serverlogotyp",
460460+ "logoPreview": "Förhandsgranskning av logotyp",
461461+ "removeLogo": "Ta bort",
462462+ "logoHelp": "Används som favicon och visas i navigeringsfältet",
463463+ "themeColors": "Temafärger",
464464+ "themeColorsHint": "Lämna tomt för att använda standardfärger.",
465465+ "primaryLight": "Primär (ljust läge)",
466466+ "primaryDark": "Primär (mörkt läge)",
467467+ "accentLight": "Accent (ljust läge)",
468468+ "accentDark": "Accent (mörkt läge)",
469469+ "faviconExample": "Favicon-exempel",
470470+ "configSaved": "Serverkonfiguration sparad",
471471+ "saving": "Sparar...",
472472+ "saveConfig": "Spara konfiguration",
454473 "serverStats": "Serverstatistik",
455474 "users": "Användare",
456475 "repos": "Dataförvar",
···514533 "readProfile": "Läsa din profilinformation",
515534 "readPosts": "Läsa dina inlägg och innehåll",
516535 "writePosts": "Skapa och radera inlägg för din räkning",
517517- "readNotifications": "Läsa dina notiser",
536536+ "readNotifications": "Läsa dina aviseringar",
518537 "fullAccess": "Full tillgång till ditt konto",
519538 "authorize": "Auktorisera",
520539 "deny": "Neka",
···580599 "verify": {
581600 "title": "Verifiera ditt konto",
582601 "subtitle": "Vi har skickat en verifieringskod till din {channel}. Ange den nedan för att slutföra registreringen.",
583583- "codePlaceholder": "Ange 6-siffrig kod",
602602+ "tokenTitle": "Verifiera",
603603+ "tokenSubtitle": "Ange verifieringskoden och identifieraren den skickades till.",
604604+ "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
584605 "codeLabel": "Verifieringskod",
606606+ "codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck",
585607 "verifyButton": "Verifiera konto",
608608+ "verify": "Verifiera",
586609 "verifying": "Verifierar...",
610610+ "pleaseWait": "Vänta...",
611611+ "sending": "Skickar...",
587612 "resendCode": "Skicka kod igen",
588613 "resending": "Skickar igen...",
589614 "codeResent": "Verifieringskod skickad igen!",
615615+ "codeResentDetail": "Verifieringskod skickad! Kontrollera din inkorg.",
616616+ "verified": "Verifierad!",
617617+ "channelVerified": "Din {channel} har verifierats.",
618618+ "canNowSignIn": "Du kan nu logga in på ditt konto.",
619619+ "continue": "Fortsätt",
620620+ "identifierLabel": "E-post eller identifierare",
621621+ "identifierPlaceholder": "du@exempel.se",
622622+ "identifierHelp": "E-postadressen eller identifieraren koden skickades till",
590623 "backToLogin": "Tillbaka till inloggning",
591624 "verifyingAccount": "Verifierar konto: @{handle}",
592625 "startOver": "Börja om med ett annat konto",
···604637 "emailPlaceholder": "användarnamn eller du@exempel.se",
605638 "sendCode": "Skicka återställningskod",
606639 "sending": "Skickar...",
607607- "codeSent": "Återställningskod skickad! Kontrollera din föredragna notifieringskanal.",
608608- "enterCode": "Ange koden från din e-post och ditt nya lösenord.",
640640+ "codeSent": "Återställningskod skickad! Kontrollera din föredragna meddelandekanal.",
641641+ "enterCode": "Ange koden du fick och ditt nya lösenord.",
609642 "code": "Återställningskod",
610643 "codePlaceholder": "Ange återställningskod",
611644 "newPassword": "Nytt lösenord",
···652685 "title": "Återställ nyckelkonto",
653686 "subtitle": "Förlorat tillgång till din nyckel? Ange ditt användarnamn eller e-post så skickar vi dig en återställningslänk.",
654687 "successTitle": "Återställningslänk skickad",
655655- "successMessage": "Om ditt konto finns och är ett endast nyckelkonto får du en återställningslänk på din föredragna notifieringskanal.",
688688+ "successMessage": "Om ditt konto finns och är ett endast nyckelkonto får du en återställningslänk på din föredragna meddelandekanal.",
656689 "successInfo": "Länken upphör om 1 timme. Kontrollera din e-post, Discord, Telegram eller Signal beroende på dina kontoinställningar.",
657690 "handleOrEmail": "Användarnamn eller e-post",
658691 "emailPlaceholder": "användarnamn eller du@exempel.se",
659692 "howItWorks": "Så fungerar det",
660660- "howItWorksDetail": "Vi skickar en säker länk till din registrerade notifieringskanal. Klicka på länken för att ställa in ett tillfälligt lösenord. Sedan kan du logga in och lägga till en ny nyckel.",
693693+ "howItWorksDetail": "Vi skickar en säker länk till din registrerade meddelandekanal. Klicka på länken för att ställa in ett tillfälligt lösenord. Sedan kan du logga in och lägga till en ny nyckel.",
661694 "sendRecoveryLink": "Skicka återställningslänk",
662695 "sending": "Skickar...",
663696 "backToLogin": "Tillbaka till inloggning"
664697 },
665698 "registerPasskey": {
666699 "title": "Skapa nyckelkonto",
667667- "subtitle": "Skapa ett lösenordsfritt konto med en nyckel.",
700700+ "subtitle": "Skapa ett ultrasäkert konto med en nyckel istället för ett lösenord.",
701701+ "subtitleKeyChoice": "Välj hur du vill konfigurera din externa did:web-identitet.",
702702+ "subtitleVerify": "Vi har skickat en verifieringskod till din {channel}. Ange koden för att fortsätta.",
703703+ "subtitlePasskey": "Skapa din nyckel för att slutföra kontokonfigurationen.",
668704 "handle": "Användarnamn",
669705 "handlePlaceholder": "dittnamn",
670706 "handleHint": "Ditt fullständiga användarnamn blir: @{handle}",
707707+ "contactMethod": "Kontaktmetod",
708708+ "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden.",
709709+ "verificationMethod": "Verifieringsmetod",
671710 "email": "E-postadress",
672711 "emailPlaceholder": "du@exempel.se",
712712+ "discord": "Discord",
713713+ "discordId": "Discord användar-ID",
714714+ "discordIdPlaceholder": "Ditt Discord användar-ID",
715715+ "discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
716716+ "telegram": "Telegram",
717717+ "telegramUsername": "Telegram-användarnamn",
718718+ "telegramUsernamePlaceholder": "@dittanvändarnamn",
719719+ "signal": "Signal",
720720+ "signalNumber": "Signal-telefonnummer",
721721+ "signalNumberPlaceholder": "+46701234567",
722722+ "signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
673723 "inviteCode": "Inbjudningskod",
674724 "inviteCodePlaceholder": "Ange din inbjudningskod",
725725+ "inviteCodeRequired": "krävs",
726726+ "didWebDescription": "Använd en DID-identitet som är lagrad på din egen domän.",
727727+ "didWebToggle": "Använd extern did:web",
728728+ "externalDid": "Din did:web",
729729+ "externalDidPlaceholder": "did:web:dindomän.se",
730730+ "dnsVerificationInstructions": "För att verifiera din domän, lägg till denna TXT-post:",
731731+ "copyDid": "Kopiera DID",
675732 "createButton": "Skapa konto",
676733 "creating": "Skapar...",
677734 "alreadyHaveAccount": "Har du redan ett konto?",
678735 "signIn": "Logga in",
679736 "wantPassword": "Vill du använda ett lösenord?",
680680- "createPasswordAccount": "Skapa ett lösenordskonto"
737737+ "createPasswordAccount": "Skapa ett lösenordskonto",
738738+ "errors": {
739739+ "handleRequired": "Användarnamn krävs",
740740+ "handleNoDots": "Användarnamn kan inte innehålla punkter. Du kan konfigurera ett eget domännamn efter att kontot skapats.",
741741+ "passkeysNotSupported": "Nycklar stöds inte i denna webbläsare. Skapa ett lösenordsbaserat konto eller använd en webbläsare som stöder nycklar.",
742742+ "passkeyCancelled": "Nyckelskapande avbröts",
743743+ "passkeyFailed": "Nyckelregistrering misslyckades"
744744+ }
681745 },
682746 "trustedDevices": {
683747 "title": "Betrodda enheter",
···710774 "verify": "Verifiera",
711775 "verifying": "Verifierar...",
712776 "cancel": "Avbryt"
777777+ },
778778+ "verifyChannel": {
779779+ "title": "Verifiera kanal",
780780+ "subtitle": "Ange verifieringskoden som skickades till din meddelandekanal.",
781781+ "signInRequired": "Inloggning krävs",
782782+ "signInRequiredDesc": "Du måste vara inloggad för att verifiera en kanal.",
783783+ "signIn": "Logga in",
784784+ "verifying": "Verifierar...",
785785+ "pleaseWait": "Vänta medan vi verifierar din kanal.",
786786+ "successTitle": "Verifierad!",
787787+ "successDesc": "Din {channel} har verifierats.",
788788+ "backToSettings": "Tillbaka till inställningar",
789789+ "channelLabel": "Kanal",
790790+ "selectChannel": "Välj kanal...",
791791+ "identifierLabel": "Identifierare",
792792+ "identifierPlaceholder": "E-post, Discord ID, etc.",
793793+ "identifierHelp": "E-postadressen, Discord ID, Telegram-användarnamn eller Signal-nummer som verifieras.",
794794+ "codeLabel": "Verifieringskod",
795795+ "codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck.",
796796+ "verifyButton": "Verifiera"
713797 }
714798}
···1212pub mod service;
1313pub mod token;
1414pub mod totp;
1515+pub mod verification_token;
1516pub mod verify;
1617pub mod webauthn;
1718
+423
src/auth/verification_token.rs
···11+use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
22+use hmac::Mac;
33+use sha2::{Digest, Sha256};
44+55+type HmacSha256 = hmac::Hmac<Sha256>;
66+77+const TOKEN_VERSION: u8 = 1;
88+const DEFAULT_SIGNUP_EXPIRY_MINUTES: u64 = 30;
99+const DEFAULT_MIGRATION_EXPIRY_HOURS: u64 = 48;
1010+const DEFAULT_CHANNEL_UPDATE_EXPIRY_MINUTES: u64 = 10;
1111+1212+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1313+pub enum VerificationPurpose {
1414+ Signup,
1515+ Migration,
1616+ ChannelUpdate,
1717+}
1818+1919+impl VerificationPurpose {
2020+ fn as_str(&self) -> &'static str {
2121+ match self {
2222+ Self::Signup => "signup",
2323+ Self::Migration => "migration",
2424+ Self::ChannelUpdate => "channel_update",
2525+ }
2626+ }
2727+2828+ fn from_str(s: &str) -> Option<Self> {
2929+ match s {
3030+ "signup" => Some(Self::Signup),
3131+ "migration" => Some(Self::Migration),
3232+ "channel_update" => Some(Self::ChannelUpdate),
3333+ _ => None,
3434+ }
3535+ }
3636+3737+ fn default_expiry_seconds(&self) -> u64 {
3838+ match self {
3939+ Self::Signup => DEFAULT_SIGNUP_EXPIRY_MINUTES * 60,
4040+ Self::Migration => DEFAULT_MIGRATION_EXPIRY_HOURS * 3600,
4141+ Self::ChannelUpdate => DEFAULT_CHANNEL_UPDATE_EXPIRY_MINUTES * 60,
4242+ }
4343+ }
4444+}
4545+4646+#[derive(Debug)]
4747+pub struct VerificationToken {
4848+ pub did: String,
4949+ pub purpose: VerificationPurpose,
5050+ pub channel: String,
5151+ pub identifier_hash: String,
5252+ pub expires_at: u64,
5353+}
5454+5555+fn derive_verification_key() -> [u8; 32] {
5656+ use hkdf::Hkdf;
5757+ let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
5858+ if cfg!(test) || std::env::var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS").is_ok() {
5959+ "test-master-key-not-for-production".to_string()
6060+ } else {
6161+ panic!("MASTER_KEY must be set");
6262+ }
6363+ });
6464+ let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
6565+ let mut key = [0u8; 32];
6666+ hk.expand(b"tranquil-pds-verification-token-v1", &mut key)
6767+ .expect("HKDF expansion failed");
6868+ key
6969+}
7070+7171+pub fn hash_identifier(identifier: &str) -> String {
7272+ let mut hasher = Sha256::new();
7373+ hasher.update(identifier.to_lowercase().as_bytes());
7474+ let result = hasher.finalize();
7575+ URL_SAFE_NO_PAD.encode(&result[..16])
7676+}
7777+7878+pub fn generate_signup_token(did: &str, channel: &str, identifier: &str) -> String {
7979+ generate_token(did, VerificationPurpose::Signup, channel, identifier)
8080+}
8181+8282+pub fn generate_migration_token(did: &str, email: &str) -> String {
8383+ generate_token(did, VerificationPurpose::Migration, "email", email)
8484+}
8585+8686+pub fn generate_channel_update_token(did: &str, channel: &str, identifier: &str) -> String {
8787+ generate_token(did, VerificationPurpose::ChannelUpdate, channel, identifier)
8888+}
8989+9090+pub fn generate_token(
9191+ did: &str,
9292+ purpose: VerificationPurpose,
9393+ channel: &str,
9494+ identifier: &str,
9595+) -> String {
9696+ generate_token_with_expiry(
9797+ did,
9898+ purpose,
9999+ channel,
100100+ identifier,
101101+ purpose.default_expiry_seconds(),
102102+ )
103103+}
104104+105105+pub fn generate_token_with_expiry(
106106+ did: &str,
107107+ purpose: VerificationPurpose,
108108+ channel: &str,
109109+ identifier: &str,
110110+ expiry_seconds: u64,
111111+) -> String {
112112+ let key = derive_verification_key();
113113+ let identifier_hash = hash_identifier(identifier);
114114+ let expires_at = std::time::SystemTime::now()
115115+ .duration_since(std::time::UNIX_EPOCH)
116116+ .unwrap_or_default()
117117+ .as_secs()
118118+ + expiry_seconds;
119119+120120+ let payload = format!(
121121+ "{}|{}|{}|{}|{}",
122122+ did,
123123+ purpose.as_str(),
124124+ channel,
125125+ identifier_hash,
126126+ expires_at
127127+ );
128128+129129+ let mut mac = <HmacSha256 as Mac>::new_from_slice(&key).expect("HMAC key size is valid");
130130+ mac.update(payload.as_bytes());
131131+ let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
132132+133133+ let token_data = format!(
134134+ "{}|{}|{}|{}|{}|{}|{}",
135135+ TOKEN_VERSION,
136136+ did,
137137+ purpose.as_str(),
138138+ channel,
139139+ identifier_hash,
140140+ expires_at,
141141+ signature
142142+ );
143143+ URL_SAFE_NO_PAD.encode(token_data.as_bytes())
144144+}
145145+146146+#[derive(Debug)]
147147+pub enum VerifyError {
148148+ InvalidFormat,
149149+ UnsupportedVersion,
150150+ Expired,
151151+ InvalidSignature,
152152+ IdentifierMismatch,
153153+ PurposeMismatch,
154154+ ChannelMismatch,
155155+}
156156+157157+impl std::fmt::Display for VerifyError {
158158+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159159+ match self {
160160+ Self::InvalidFormat => write!(f, "Invalid token format"),
161161+ Self::UnsupportedVersion => write!(f, "Unsupported token version"),
162162+ Self::Expired => write!(f, "Token has expired"),
163163+ Self::InvalidSignature => write!(f, "Invalid token signature"),
164164+ Self::IdentifierMismatch => write!(f, "Identifier does not match token"),
165165+ Self::PurposeMismatch => write!(f, "Token purpose does not match"),
166166+ Self::ChannelMismatch => write!(f, "Token channel does not match"),
167167+ }
168168+ }
169169+}
170170+171171+pub fn verify_signup_token(
172172+ token: &str,
173173+ expected_channel: &str,
174174+ expected_identifier: &str,
175175+) -> Result<VerificationToken, VerifyError> {
176176+ let parsed = verify_token_signature(token)?;
177177+ if parsed.purpose != VerificationPurpose::Signup {
178178+ return Err(VerifyError::PurposeMismatch);
179179+ }
180180+ if parsed.channel != expected_channel {
181181+ return Err(VerifyError::ChannelMismatch);
182182+ }
183183+ let expected_hash = hash_identifier(expected_identifier);
184184+ if parsed.identifier_hash != expected_hash {
185185+ return Err(VerifyError::IdentifierMismatch);
186186+ }
187187+ Ok(parsed)
188188+}
189189+190190+pub fn verify_migration_token(
191191+ token: &str,
192192+ expected_email: &str,
193193+) -> Result<VerificationToken, VerifyError> {
194194+ let parsed = verify_token_signature(token)?;
195195+ if parsed.purpose != VerificationPurpose::Migration {
196196+ return Err(VerifyError::PurposeMismatch);
197197+ }
198198+ if parsed.channel != "email" {
199199+ return Err(VerifyError::ChannelMismatch);
200200+ }
201201+ let expected_hash = hash_identifier(expected_email);
202202+ if parsed.identifier_hash != expected_hash {
203203+ return Err(VerifyError::IdentifierMismatch);
204204+ }
205205+ Ok(parsed)
206206+}
207207+208208+pub fn verify_channel_update_token(
209209+ token: &str,
210210+ expected_channel: &str,
211211+ expected_identifier: &str,
212212+) -> Result<VerificationToken, VerifyError> {
213213+ let parsed = verify_token_signature(token)?;
214214+ if parsed.purpose != VerificationPurpose::ChannelUpdate {
215215+ return Err(VerifyError::PurposeMismatch);
216216+ }
217217+ if parsed.channel != expected_channel {
218218+ return Err(VerifyError::ChannelMismatch);
219219+ }
220220+ let expected_hash = hash_identifier(expected_identifier);
221221+ if parsed.identifier_hash != expected_hash {
222222+ return Err(VerifyError::IdentifierMismatch);
223223+ }
224224+ Ok(parsed)
225225+}
226226+227227+pub fn verify_token_for_did(
228228+ token: &str,
229229+ expected_did: &str,
230230+) -> Result<VerificationToken, VerifyError> {
231231+ let parsed = verify_token_signature(token)?;
232232+ if parsed.did != expected_did {
233233+ return Err(VerifyError::IdentifierMismatch);
234234+ }
235235+ Ok(parsed)
236236+}
237237+238238+pub fn verify_token_signature(token: &str) -> Result<VerificationToken, VerifyError> {
239239+ let token_bytes = URL_SAFE_NO_PAD
240240+ .decode(token.trim())
241241+ .map_err(|_| VerifyError::InvalidFormat)?;
242242+ let token_str = String::from_utf8(token_bytes).map_err(|_| VerifyError::InvalidFormat)?;
243243+244244+ let parts: Vec<&str> = token_str.split('|').collect();
245245+ if parts.len() != 7 {
246246+ return Err(VerifyError::InvalidFormat);
247247+ }
248248+249249+ let version: u8 = parts[0].parse().map_err(|_| VerifyError::InvalidFormat)?;
250250+ if version != TOKEN_VERSION {
251251+ return Err(VerifyError::UnsupportedVersion);
252252+ }
253253+254254+ let did = parts[1];
255255+ let purpose_str = parts[2];
256256+ let channel = parts[3];
257257+ let identifier_hash = parts[4];
258258+ let expires_at: u64 = parts[5].parse().map_err(|_| VerifyError::InvalidFormat)?;
259259+ let provided_signature = parts[6];
260260+261261+ let purpose = VerificationPurpose::from_str(purpose_str).ok_or(VerifyError::InvalidFormat)?;
262262+263263+ let now = std::time::SystemTime::now()
264264+ .duration_since(std::time::UNIX_EPOCH)
265265+ .unwrap_or_default()
266266+ .as_secs();
267267+ if now > expires_at {
268268+ return Err(VerifyError::Expired);
269269+ }
270270+271271+ let key = derive_verification_key();
272272+ let payload = format!(
273273+ "{}|{}|{}|{}|{}",
274274+ did, purpose_str, channel, identifier_hash, expires_at
275275+ );
276276+ let mut mac = <HmacSha256 as Mac>::new_from_slice(&key).expect("HMAC key size is valid");
277277+ mac.update(payload.as_bytes());
278278+ let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
279279+280280+ use subtle::ConstantTimeEq;
281281+ let sig_matches: bool = provided_signature
282282+ .as_bytes()
283283+ .ct_eq(expected_signature.as_bytes())
284284+ .into();
285285+ if !sig_matches {
286286+ return Err(VerifyError::InvalidSignature);
287287+ }
288288+289289+ Ok(VerificationToken {
290290+ did: did.to_string(),
291291+ purpose,
292292+ channel: channel.to_string(),
293293+ identifier_hash: identifier_hash.to_string(),
294294+ expires_at,
295295+ })
296296+}
297297+298298+pub fn format_token_for_display(token: &str) -> String {
299299+ let clean = token.replace(['-', ' '], "");
300300+ let mut result = String::new();
301301+ for (i, c) in clean.chars().enumerate() {
302302+ if i > 0 && i % 4 == 0 {
303303+ result.push('-');
304304+ }
305305+ result.push(c);
306306+ }
307307+ result
308308+}
309309+310310+pub fn normalize_token_input(input: &str) -> String {
311311+ input
312312+ .chars()
313313+ .filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '=')
314314+ .collect()
315315+}
316316+317317+#[cfg(test)]
318318+mod tests {
319319+ use super::*;
320320+321321+ #[test]
322322+ fn test_signup_token() {
323323+ let did = "did:plc:test123";
324324+ let channel = "email";
325325+ let identifier = "test@example.com";
326326+ let token = generate_signup_token(did, channel, identifier);
327327+ let result = verify_signup_token(&token, channel, identifier);
328328+ assert!(result.is_ok(), "Expected Ok, got {:?}", result);
329329+ let parsed = result.unwrap();
330330+ assert_eq!(parsed.did, did);
331331+ assert_eq!(parsed.purpose, VerificationPurpose::Signup);
332332+ assert_eq!(parsed.channel, channel);
333333+ }
334334+335335+ #[test]
336336+ fn test_migration_token() {
337337+ let did = "did:plc:test123";
338338+ let email = "test@example.com";
339339+ let token = generate_migration_token(did, email);
340340+ let result = verify_migration_token(&token, email);
341341+ assert!(result.is_ok(), "Expected Ok, got {:?}", result);
342342+ let parsed = result.unwrap();
343343+ assert_eq!(parsed.did, did);
344344+ assert_eq!(parsed.purpose, VerificationPurpose::Migration);
345345+ }
346346+347347+ #[test]
348348+ fn test_token_case_insensitive() {
349349+ let did = "did:plc:test123";
350350+ let token = generate_signup_token(did, "email", "Test@Example.COM");
351351+ let result = verify_signup_token(&token, "email", "test@example.com");
352352+ assert!(result.is_ok());
353353+ }
354354+355355+ #[test]
356356+ fn test_token_wrong_identifier() {
357357+ let did = "did:plc:test123";
358358+ let token = generate_signup_token(did, "email", "test@example.com");
359359+ let result = verify_signup_token(&token, "email", "other@example.com");
360360+ assert!(matches!(result, Err(VerifyError::IdentifierMismatch)));
361361+ }
362362+363363+ #[test]
364364+ fn test_token_wrong_channel() {
365365+ let did = "did:plc:test123";
366366+ let token = generate_signup_token(did, "email", "test@example.com");
367367+ let result = verify_signup_token(&token, "discord", "test@example.com");
368368+ assert!(matches!(result, Err(VerifyError::ChannelMismatch)));
369369+ }
370370+371371+ #[test]
372372+ fn test_expired_token() {
373373+ let did = "did:plc:test123";
374374+ let token = generate_token_with_expiry(
375375+ did,
376376+ VerificationPurpose::Signup,
377377+ "email",
378378+ "test@example.com",
379379+ 0,
380380+ );
381381+ std::thread::sleep(std::time::Duration::from_millis(1100));
382382+ let result = verify_signup_token(&token, "email", "test@example.com");
383383+ assert!(matches!(result, Err(VerifyError::Expired)));
384384+ }
385385+386386+ #[test]
387387+ fn test_invalid_token() {
388388+ let result = verify_signup_token("invalid-token", "email", "test@example.com");
389389+ assert!(matches!(result, Err(VerifyError::InvalidFormat)));
390390+ }
391391+392392+ #[test]
393393+ fn test_purpose_mismatch() {
394394+ let did = "did:plc:test123";
395395+ let email = "test@example.com";
396396+ let signup_token = generate_signup_token(did, "email", email);
397397+ let result = verify_migration_token(&signup_token, email);
398398+ assert!(matches!(result, Err(VerifyError::PurposeMismatch)));
399399+ }
400400+401401+ #[test]
402402+ fn test_discord_channel() {
403403+ let did = "did:plc:test123";
404404+ let discord_id = "123456789012345678";
405405+ let token = generate_signup_token(did, "discord", discord_id);
406406+ let result = verify_signup_token(&token, "discord", discord_id);
407407+ assert!(result.is_ok());
408408+ }
409409+410410+ #[test]
411411+ fn test_format_token_for_display() {
412412+ let token = "ABCDEFGHIJKLMNOP";
413413+ let formatted = format_token_for_display(token);
414414+ assert_eq!(formatted, "ABCD-EFGH-IJKL-MNOP");
415415+ }
416416+417417+ #[test]
418418+ fn test_normalize_token_input() {
419419+ let input = "ABCD-EFGH IJKL-MNOP";
420420+ let normalized = normalize_token_input(input);
421421+ assert_eq!(normalized, "ABCDEFGHIJKLMNOP");
422422+ }
423423+}
+28-28
src/comms/locale.rs
···1212pub struct NotificationStrings {
1313 pub welcome_subject: &'static str,
1414 pub welcome_body: &'static str,
1515- pub email_verification_subject: &'static str,
1616- pub email_verification_body: &'static str,
1715 pub password_reset_subject: &'static str,
1816 pub password_reset_body: &'static str,
1917 pub email_update_subject: &'static str,
···3028 pub signup_verification_body: &'static str,
3129 pub legacy_login_subject: &'static str,
3230 pub legacy_login_body: &'static str,
3131+ pub migration_verification_subject: &'static str,
3232+ pub migration_verification_body: &'static str,
3333}
34343535pub fn get_strings(locale: &str) -> &'static NotificationStrings {
···4646static STRINGS_EN: NotificationStrings = NotificationStrings {
4747 welcome_subject: "Welcome to {hostname}",
4848 welcome_body: "Welcome to {hostname}!\n\nYour handle is: @{handle}\n\nThank you for joining us.",
4949- email_verification_subject: "Verify your email - {hostname}",
5050- email_verification_body: "Hello @{handle},\n\nYour email verification code is: {code}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this email.",
5149 password_reset_subject: "Password Reset - {hostname}",
5250 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.",
5351 email_update_subject: "Confirm your new email - {hostname}",
5454- email_update_body: "Hello @{handle},\n\nYour email update confirmation code is: {code}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this email.",
5252+ 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})",
5553 account_deletion_subject: "Account Deletion Request - {hostname}",
5654 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.",
5755 plc_operation_subject: "{hostname} - PLC Operation Token",
···6159 passkey_recovery_subject: "Account Recovery - {hostname}",
6260 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.",
6361 signup_verification_subject: "Verify your account - {hostname}",
6464- signup_verification_body: "Welcome! Your account verification code is: {code}\n\nThis code will expire in 30 minutes.\n\nEnter this code to complete your registration on {hostname}.",
6262+ 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})",
6563 legacy_login_subject: "Security Alert: Legacy Login Detected - {hostname}",
6664 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}",
6565+ migration_verification_subject: "Verify your email - {hostname}",
6666+ 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})",
6767};
68686969static STRINGS_ZH: NotificationStrings = NotificationStrings {
7070 welcome_subject: "欢迎加入 {hostname}",
7171 welcome_body: "欢迎加入 {hostname}!\n\n您的用户名是:@{handle}\n\n感谢您的加入。",
7272- email_verification_subject: "验证您的邮箱 - {hostname}",
7373- email_verification_body: "您好 @{handle},\n\n您的邮箱验证码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此邮件。",
7472 password_reset_subject: "密码重置 - {hostname}",
7573 password_reset_body: "您好 @{handle},\n\n您的密码重置验证码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此消息。",
7674 email_update_subject: "确认您的新邮箱 - {hostname}",
7777- email_update_body: "您好 @{handle},\n\n您的邮箱更新确认码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此邮件。",
7575+ email_update_body: "您好 @{handle},\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请忽略此邮件。\n\n(或者直接点击链接:{verify_link})",
7876 account_deletion_subject: "账户删除请求 - {hostname}",
7977 account_deletion_body: "您好 @{handle},\n\n您的账户删除确认码是:{code}\n\n此验证码将在10分钟后过期。\n\n如果这不是您的操作,请立即保护您的账户。",
8078 plc_operation_subject: "{hostname} - PLC 操作令牌",
···8482 passkey_recovery_subject: "账户恢复 - {hostname}",
8583 passkey_recovery_body: "您好 @{handle},\n\n您请求恢复仅通行密钥账户的访问权限。\n\n点击以下链接设置临时密码并恢复访问:\n{url}\n\n此链接将在1小时后过期。\n\n如果这不是您的操作,请忽略此消息。您的账户仍然安全。",
8684 signup_verification_subject: "验证您的账户 - {hostname}",
8787- signup_verification_body: "欢迎!您的账户验证码是:{code}\n\n此验证码将在30分钟后过期。\n\n请输入此验证码完成在 {hostname} 上的注册。",
8585+ signup_verification_body: "欢迎!您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在30分钟后过期。\n\n如果您没有在 {hostname} 上创建账户,请忽略此消息。\n\n(或者直接点击链接:{verify_link})",
8886 legacy_login_subject: "安全提醒:检测到传统应用登录 - {hostname}",
8987 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}",
8888+ migration_verification_subject: "验证您的邮箱 - {hostname}",
8989+ migration_verification_body: "欢迎来到 {hostname}!\n\n您的账户已成功迁移。要完成设置,请验证您的邮箱地址。\n\n您的验证码是:\n{code}\n\n复制上述验证码并在此输入:\n{verify_page}\n\n此验证码将在 48 小时后过期。\n\n如果您没有迁移账户,请忽略此邮件。\n\n(或者直接点击链接:{verify_link})",
9090};
91919292static STRINGS_JA: NotificationStrings = NotificationStrings {
9393 welcome_subject: "{hostname} へようこそ",
9494 welcome_body: "{hostname} へようこそ!\n\nお客様のハンドル:@{handle}\n\nご登録ありがとうございます。",
9595- email_verification_subject: "メール認証 - {hostname}",
9696- email_verification_body: "@{handle} 様\n\nメール認証コードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメールを無視してください。",
9795 password_reset_subject: "パスワードリセット - {hostname}",
9896 password_reset_body: "@{handle} 様\n\nパスワードリセットコードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメッセージを無視してください。",
9997 email_update_subject: "新しいメールアドレスの確認 - {hostname}",
100100- email_update_body: "@{handle} 様\n\nメールアドレス更新の確認コードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメールを無視してください。",
9898+ email_update_body: "@{handle} 様\n\n確認コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})",
10199 account_deletion_subject: "アカウント削除リクエスト - {hostname}",
102100 account_deletion_body: "@{handle} 様\n\nアカウント削除の確認コードは:{code}\n\nこのコードは10分後に期限切れとなります。\n\nこの操作に心当たりがない場合は、直ちにアカウントを保護してください。",
103101 plc_operation_subject: "{hostname} - PLC 操作トークン",
···107105 passkey_recovery_subject: "アカウント復旧 - {hostname}",
108106 passkey_recovery_body: "@{handle} 様\n\nパスキー専用アカウントの復旧をリクエストされました。\n\n以下のリンクをクリックして一時パスワードを設定し、アクセスを回復してください:\n{url}\n\nこのリンクは1時間後に期限切れとなります。\n\nこの操作に心当たりがない場合は、このメッセージを無視してください。アカウントは安全なままです。",
109107 signup_verification_subject: "アカウント認証 - {hostname}",
110110- signup_verification_body: "ようこそ!アカウント認証コードは:{code}\n\nこのコードは30分後に期限切れとなります。\n\n{hostname} への登録を完了するには、このコードを入力してください。",
108108+ signup_verification_body: "ようこそ!認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは30分後に期限切れとなります。\n\n{hostname} でアカウントを作成していない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})",
111109 legacy_login_subject: "セキュリティ警告:レガシーログインを検出 - {hostname}",
112110 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}",
111111+ migration_verification_subject: "メールアドレスの認証 - {hostname}",
112112+ migration_verification_body: "{hostname} へようこそ!\n\nアカウントの移行が完了しました。設定を完了するには、メールアドレスを認証してください。\n\n認証コードは:\n{code}\n\n上記のコードをコピーして、こちらで入力してください:\n{verify_page}\n\nこのコードは48時間後に期限切れとなります。\n\nアカウントを移行していない場合は、このメールを無視してください。\n\n(自己責任でワンクリック認証:{verify_link})",
113113};
114114115115static STRINGS_KO: NotificationStrings = NotificationStrings {
116116 welcome_subject: "{hostname}에 오신 것을 환영합니다",
117117 welcome_body: "{hostname}에 오신 것을 환영합니다!\n\n회원님의 핸들은: @{handle}\n\n가입해 주셔서 감사합니다.",
118118- email_verification_subject: "이메일 인증 - {hostname}",
119119- email_verification_body: "안녕하세요 @{handle}님,\n\n이메일 인증 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 이메일을 무시하세요.",
120118 password_reset_subject: "비밀번호 재설정 - {hostname}",
121119 password_reset_body: "안녕하세요 @{handle}님,\n\n비밀번호 재설정 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 메시지를 무시하세요.",
122122- email_update_subject: "새 이메일 확인 - {hostname}",
123123- email_update_body: "안녕하세요 @{handle}님,\n\n이메일 업데이트 확인 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 이메일을 무시하세요.",
120120+ email_update_subject: "새 이메일 주소 확인 - {hostname}",
121121+ email_update_body: "안녕하세요 @{handle}님,\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})",
124122 account_deletion_subject: "계정 삭제 요청 - {hostname}",
125123 account_deletion_body: "안녕하세요 @{handle}님,\n\n계정 삭제 확인 코드는: {code}\n\n이 코드는 10분 후에 만료됩니다.\n\n요청하지 않으셨다면 즉시 계정을 보호하세요.",
126124 plc_operation_subject: "{hostname} - PLC 작업 토큰",
···130128 passkey_recovery_subject: "계정 복구 - {hostname}",
131129 passkey_recovery_body: "안녕하세요 @{handle}님,\n\n패스키 전용 계정 복구를 요청하셨습니다.\n\n아래 링크를 클릭하여 임시 비밀번호를 설정하고 액세스를 복구하세요:\n{url}\n\n이 링크는 1시간 후에 만료됩니다.\n\n요청하지 않으셨다면 이 메시지를 무시하세요. 계정은 안전하게 유지됩니다.",
132130 signup_verification_subject: "계정 인증 - {hostname}",
133133- signup_verification_body: "환영합니다! 계정 인증 코드는: {code}\n\n이 코드는 30분 후에 만료됩니다.\n\n{hostname}에서 등록을 완료하려면 이 코드를 입력하세요.",
131131+ signup_verification_body: "환영합니다! 인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 30분 후에 만료됩니다.\n\n{hostname}에서 계정을 만들지 않았다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})",
134132 legacy_login_subject: "보안 알림: 레거시 로그인 감지 - {hostname}",
135133 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} 드림",
134134+ migration_verification_subject: "이메일 인증 - {hostname}",
135135+ migration_verification_body: "{hostname}에 오신 것을 환영합니다!\n\n계정 마이그레이션이 완료되었습니다. 설정을 완료하려면 이메일 주소를 인증하세요.\n\n인증 코드는:\n{code}\n\n위 코드를 복사하여 여기에 입력하세요:\n{verify_page}\n\n이 코드는 48시간 후에 만료됩니다.\n\n계정을 마이그레이션하지 않았다면 이 이메일을 무시하세요.\n\n(위험을 감수하고 원클릭 인증: {verify_link})",
136136};
137137138138static STRINGS_SV: NotificationStrings = NotificationStrings {
139139 welcome_subject: "Välkommen till {hostname}",
140140 welcome_body: "Välkommen till {hostname}!\n\nDitt användarnamn är: @{handle}\n\nTack för att du gick med.",
141141- email_verification_subject: "Verifiera din e-post - {hostname}",
142142- email_verification_body: "Hej @{handle},\n\nDin e-postverifieringskod är: {code}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta kan du ignorera detta meddelande.",
143141 password_reset_subject: "Lösenordsåterställning - {hostname}",
144142 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.",
145143 email_update_subject: "Bekräfta din nya e-post - {hostname}",
146146- email_update_body: "Hej @{handle},\n\nDin bekräftelsekod för e-postuppdatering är: {code}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta kan du ignorera detta meddelande.",
144144+ 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})",
147145 account_deletion_subject: "Begäran om kontoradering - {hostname}",
148146 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.",
149147 plc_operation_subject: "{hostname} - PLC-operationstoken",
···153151 passkey_recovery_subject: "Kontoåterställning - {hostname}",
154152 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.",
155153 signup_verification_subject: "Verifiera ditt konto - {hostname}",
156156- signup_verification_body: "Välkommen! Din kontoverifieringskod är: {code}\n\nDenna kod upphör om 30 minuter.\n\nAnge denna kod för att slutföra din registrering på {hostname}.",
154154+ 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})",
157155 legacy_login_subject: "Säkerhetsvarning: Äldre inloggning upptäckt - {hostname}",
158156 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}",
157157+ migration_verification_subject: "Verifiera din e-post - {hostname}",
158158+ 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})",
159159};
160160161161static STRINGS_FI: NotificationStrings = NotificationStrings {
162162 welcome_subject: "Tervetuloa palveluun {hostname}",
163163 welcome_body: "Tervetuloa palveluun {hostname}!\n\nKäyttäjänimesi on: @{handle}\n\nKiitos liittymisestä.",
164164- email_verification_subject: "Vahvista sähköpostisi - {hostname}",
165165- email_verification_body: "Hei @{handle},\n\nSähköpostin vahvistuskoodisi on: {code}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta.",
166164 password_reset_subject: "Salasanan palautus - {hostname}",
167165 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.",
168168- email_update_subject: "Vahvista uusi sähköpostiosoitteesi - {hostname}",
169169- email_update_body: "Hei @{handle},\n\nSähköpostin päivityksen vahvistuskoodisi on: {code}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, voit jättää tämän viestin huomiotta.",
166166+ email_update_subject: "Vahvista uusi sähköpostisi - {hostname}",
167167+ 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})",
170168 account_deletion_subject: "Tilin poistopyyntö - {hostname}",
171169 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.",
172170 plc_operation_subject: "{hostname} - PLC-toimintotunniste",
···176174 passkey_recovery_subject: "Tilin palautus - {hostname}",
177175 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.",
178176 signup_verification_subject: "Vahvista tilisi - {hostname}",
179179- signup_verification_body: "Tervetuloa! Tilin vahvistuskoodisi on: {code}\n\nTämä koodi vanhenee 30 minuutissa.\n\nSyötä tämä koodi viimeistelläksesi rekisteröintisi palveluun {hostname}.",
177177+ 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})",
180178 legacy_login_subject: "Turvallisuushälytys: Vanha kirjautuminen havaittu - {hostname}",
181179 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}",
180180+ migration_verification_subject: "Vahvista sähköpostisi - {hostname}",
181181+ 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})",
182182};
183183184184pub fn format_message(template: &str, vars: &[(&str, &str)]) -> String {