this repo has no description

Finnish & Swedish translations

lewis 13bf4455 5878fa81

+22 -7
frontend/src/components/ReauthModal.svelte
··· 1 1 <script lang="ts"> 2 - import { getAuthState } from '../lib/auth.svelte' 2 + import { getAuthState, getValidToken } from '../lib/auth.svelte' 3 3 import { api, ApiError } from '../lib/api' 4 4 import { _ } from '../lib/i18n' 5 5 ··· 74 74 loading = true 75 75 error = '' 76 76 try { 77 - await api.reauthPassword(auth.session.accessJwt, password) 77 + const token = await getValidToken() 78 + if (!token) { 79 + error = 'Session expired. Please log in again.' 80 + return 81 + } 82 + await api.reauthPassword(token, password) 78 83 show = false 79 84 onSuccess() 80 85 } catch (e) { ··· 90 95 loading = true 91 96 error = '' 92 97 try { 93 - await api.reauthTotp(auth.session.accessJwt, totpCode) 98 + const token = await getValidToken() 99 + if (!token) { 100 + error = 'Session expired. Please log in again.' 101 + return 102 + } 103 + await api.reauthTotp(token, totpCode) 94 104 show = false 95 105 onSuccess() 96 106 } catch (e) { ··· 109 119 loading = true 110 120 error = '' 111 121 try { 112 - const { options } = await api.reauthPasskeyStart(auth.session.accessJwt) 122 + const token = await getValidToken() 123 + if (!token) { 124 + error = 'Session expired. Please log in again.' 125 + return 126 + } 127 + const { options } = await api.reauthPasskeyStart(token) 113 128 const publicKeyOptions = prepareAuthOptions(options) 114 129 const credential = await navigator.credentials.get({ 115 130 publicKey: publicKeyOptions ··· 131 146 userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : null, 132 147 }, 133 148 } 134 - await api.reauthPasskeyFinish(auth.session.accessJwt, credentialResponse) 149 + await api.reauthPasskeyFinish(token, credentialResponse) 135 150 show = false 136 151 onSuccess() 137 152 } catch (e) { ··· 152 167 </script> 153 168 154 169 {#if show} 155 - <div class="modal-backdrop" onclick={handleClose} role="presentation"> 156 - <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true"> 170 + <div class="modal-backdrop" onclick={handleClose} onkeydown={(e) => e.key === 'Escape' && handleClose()} role="presentation"> 171 + <div class="modal" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1"> 157 172 <div class="modal-header"> 158 173 <h2>Re-authentication Required</h2> 159 174 <button class="close-btn" onclick={handleClose} aria-label="Close">&times;</button>
+14 -1
frontend/src/lib/api.ts
··· 11 11 } 12 12 } 13 13 14 + let tokenRefreshCallback: (() => Promise<string | null>) | null = null 15 + 16 + export function setTokenRefreshCallback(callback: () => Promise<string | null>) { 17 + tokenRefreshCallback = callback 18 + } 19 + 14 20 async function xrpc<T>(method: string, options?: { 15 21 method?: 'GET' | 'POST' 16 22 params?: Record<string, string> 17 23 body?: unknown 18 24 token?: string 25 + skipRetry?: boolean 19 26 }): Promise<T> { 20 - const { method: httpMethod = 'GET', params, body, token } = options ?? {} 27 + const { method: httpMethod = 'GET', params, body, token, skipRetry } = options ?? {} 21 28 let url = `${API_BASE}/${method}` 22 29 if (params) { 23 30 const searchParams = new URLSearchParams(params) ··· 37 44 }) 38 45 if (!res.ok) { 39 46 const err = await res.json().catch(() => ({ error: 'Unknown', message: res.statusText })) 47 + if (res.status === 401 && err.error === 'AuthenticationFailed' && token && tokenRefreshCallback && !skipRetry) { 48 + const newToken = await tokenRefreshCallback() 49 + if (newToken && newToken !== token) { 50 + return xrpc(method, { ...options, token: newToken, skipRetry: true }) 51 + } 52 + } 40 53 throw new ApiError(res.status, err.error, err.message, err.did, err.reauthMethods) 41 54 } 42 55 return res.json()
+51 -2
frontend/src/lib/auth.svelte.ts
··· 1 - import { api, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api' 1 + import { api, setTokenRefreshCallback, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api' 2 2 import { startOAuthLogin, handleOAuthCallback, checkForOAuthCallback, clearOAuthCallbackParams, refreshOAuthToken } from './oauth' 3 3 import { setLocale, type SupportedLocale } from './i18n' 4 4 ··· 92 92 state.savedAccounts = accounts 93 93 } 94 94 95 + async function tryRefreshToken(): Promise<string | null> { 96 + if (!state.session) return null 97 + try { 98 + const tokens = await refreshOAuthToken(state.session.refreshJwt) 99 + const sessionInfo = await api.getSession(tokens.access_token) 100 + const session: Session = { 101 + ...sessionInfo, 102 + accessJwt: tokens.access_token, 103 + refreshJwt: tokens.refresh_token || state.session.refreshJwt, 104 + } 105 + state.session = session 106 + saveSession(session) 107 + addOrUpdateSavedAccount(session) 108 + return session.accessJwt 109 + } catch { 110 + return null 111 + } 112 + } 113 + 95 114 export async function initAuth() { 115 + setTokenRefreshCallback(tryRefreshToken) 96 116 state.loading = true 97 117 state.error = null 98 118 state.savedAccounts = loadSavedAccounts() ··· 142 162 saveSession(session) 143 163 addOrUpdateSavedAccount(session) 144 164 applyLocaleFromSession(sessionInfo) 145 - } catch { 165 + } catch (refreshError) { 166 + console.error('Token refresh failed during init:', refreshError) 146 167 saveSession(null) 147 168 state.session = null 148 169 } 149 170 } else { 171 + console.error('Non-401 error during getSession:', e) 150 172 saveSession(null) 151 173 state.session = null 152 174 } ··· 319 341 320 342 export function getToken(): string | null { 321 343 return state.session?.accessJwt ?? null 344 + } 345 + 346 + export async function getValidToken(): Promise<string | null> { 347 + if (!state.session) return null 348 + try { 349 + await api.getSession(state.session.accessJwt) 350 + return state.session.accessJwt 351 + } catch (e) { 352 + if (e instanceof ApiError && e.status === 401) { 353 + try { 354 + const tokens = await refreshOAuthToken(state.session.refreshJwt) 355 + const sessionInfo = await api.getSession(tokens.access_token) 356 + const session: Session = { 357 + ...sessionInfo, 358 + accessJwt: tokens.access_token, 359 + refreshJwt: tokens.refresh_token || state.session.refreshJwt, 360 + } 361 + state.session = session 362 + saveSession(session) 363 + addOrUpdateSavedAccount(session) 364 + return session.accessJwt 365 + } catch { 366 + return null 367 + } 368 + } 369 + return null 370 + } 322 371 } 323 372 324 373 export function isAuthenticated(): boolean {
+6 -2
frontend/src/lib/i18n.ts
··· 2 2 3 3 const LOCALE_STORAGE_KEY = 'tranquil-pds-locale' 4 4 5 - const SUPPORTED_LOCALES = ['en', 'zh', 'ja', 'ko'] as const 5 + const SUPPORTED_LOCALES = ['en', 'zh', 'ja', 'ko', 'sv', 'fi'] as const 6 6 export type SupportedLocale = typeof SUPPORTED_LOCALES[number] 7 7 8 8 export const localeNames: Record<SupportedLocale, string> = { 9 9 en: 'English', 10 10 zh: '中文', 11 11 ja: '日本語', 12 - ko: '한국어' 12 + ko: '한국어', 13 + sv: 'Svenska', 14 + fi: 'Suomi' 13 15 } 14 16 15 17 register('en', () => import('../locales/en.json')) 16 18 register('zh', () => import('../locales/zh.json')) 17 19 register('ja', () => import('../locales/ja.json')) 18 20 register('ko', () => import('../locales/ko.json')) 21 + register('sv', () => import('../locales/sv.json')) 22 + register('fi', () => import('../locales/fi.json')) 19 23 20 24 function getInitialLocale(): string { 21 25 const stored = localStorage.getItem(LOCALE_STORAGE_KEY)
+2
frontend/src/locales/en.json
··· 306 306 "legacyLoginDescription": "Allow signing in with username/password directly (legacy mode). When disabled, you must use OAuth with MFA.", 307 307 "legacyLoginOn": "Legacy login is enabled", 308 308 "legacyLoginOff": "Legacy login is disabled", 309 + "enableLegacyLogin": "Enable legacy login", 310 + "disableLegacyLogin": "Disable legacy login", 309 311 "legacyLoginWarning": "Warning: Enabling legacy login bypasses MFA for direct password logins. Only enable if needed for app compatibility.", 310 312 "totpPasswordWarning": "With TOTP enabled, changing your password from the Bluesky app (or other legacy apps) will be blocked. To change your password, you have two options:", 311 313 "totpPasswordOption1Label": "Change it here:",
+704
frontend/src/locales/fi.json
··· 1 + { 2 + "common": { 3 + "loading": "Ladataan...", 4 + "error": "Virhe", 5 + "save": "Tallenna", 6 + "cancel": "Peruuta", 7 + "back": "Takaisin", 8 + "done": "Valmis", 9 + "refresh": "Päivitä", 10 + "create": "Luo", 11 + "delete": "Poista", 12 + "confirm": "Vahvista", 13 + "created": "Luotu", 14 + "expires": "Vanhenee", 15 + "name": "Nimi", 16 + "dashboard": "Hallintapaneeli", 17 + "backToDashboard": "← Hallintapaneeli" 18 + }, 19 + "login": { 20 + "title": "Kirjaudu sisään", 21 + "subtitle": "Kirjaudu sisään hallitaksesi PDS-tiliäsi", 22 + "button": "Kirjaudu sisään", 23 + "redirecting": "Ohjataan...", 24 + "chooseAccount": "Valitse tili", 25 + "signInToAnother": "Kirjaudu toiselle tilille", 26 + "backToSaved": "← Takaisin tallennettuihin tileihin", 27 + "forgotPassword": "Unohditko salasanan?", 28 + "lostPasskey": "Kadotitko pääsyavaimen?", 29 + "noAccount": "Eikö sinulla ole tiliä?", 30 + "createAccount": "Luo tili", 31 + "removeAccount": "Poista tallennetuista tileistä" 32 + }, 33 + "verification": { 34 + "title": "Vahvista tilisi", 35 + "subtitle": "Tilisi vaatii vahvistuksen. Syötä vahvistusmenetelmääsi lähetetty koodi.", 36 + "codeLabel": "Vahvistuskoodi", 37 + "codePlaceholder": "Syötä 6-numeroinen koodi", 38 + "verifyButton": "Vahvista tili", 39 + "verifying": "Vahvistetaan...", 40 + "resendButton": "Lähetä koodi uudelleen", 41 + "resending": "Lähetetään uudelleen...", 42 + "resent": "Vahvistuskoodi lähetetty uudelleen!", 43 + "backToLogin": "Takaisin kirjautumiseen" 44 + }, 45 + "register": { 46 + "title": "Luo tili", 47 + "subtitle": "Luo uusi tili tälle PDS:lle", 48 + "handle": "Käyttäjänimi", 49 + "handlePlaceholder": "nimesi", 50 + "handleHint": "Täydellinen käyttäjänimesi on: @{handle}", 51 + "handleDotWarning": "Omat verkkotunnukset voidaan määrittää tilin luomisen jälkeen Asetuksissa.", 52 + "password": "Salasana", 53 + "passwordPlaceholder": "Vähintään 8 merkkiä", 54 + "confirmPassword": "Vahvista salasana", 55 + "confirmPasswordPlaceholder": "Vahvista salasanasi", 56 + "identityType": "Identiteettityyppi", 57 + "identityHint": "Valitse, miten hajautettu identiteettisi hallinnoidaan.", 58 + "didPlc": "did:plc", 59 + "didPlcRecommended": "(Suositellaan)", 60 + "didPlcHint": "Siirrettävä identiteetti, jota hallinnoi PLC Directory", 61 + "didWeb": "did:web", 62 + "didWebHint": "Identiteetti isännöidään tällä PDS:llä (lue alla oleva varoitus)", 63 + "didWebBYOD": "did:web (oma verkkotunnus)", 64 + "didWebBYODHint": "Käytä omaa verkkotunnustasi", 65 + "didWebWarningTitle": "Tärkeää: Ymmärrä kompromissit", 66 + "didWebWarning1": "Pysyvä sidonta tähän PDS:ään:", 67 + "didWebWarning1Detail": "Identiteettisi on {did}. Vaikka siirtyisit toiseen PDS:ään myöhemmin, tämän palvelimen on jatkettava DID-dokumenttisi isännöintiä.", 68 + "didWebWarning2": "Ei palautusmekanismia:", 69 + "didWebWarning2Detail": "Toisin kuin did:plc, did:web ei sisällä rotaatioavaimia. Jos tämä PDS menee pysyvästi offline-tilaan, identiteettiäsi ei voida palauttaa.", 70 + "didWebWarning3": "Sitoudumme sinuun:", 71 + "didWebWarning3Detail": "Jos siirryt pois, jatkamme minimaalisen DID-dokumentin tarjoamista, joka osoittaa uuteen PDS:ääsi. Identiteettisi pysyy toiminnassa.", 72 + "didWebWarning4": "Suositus:", 73 + "didWebWarning4Detail": "Valitse did:plc, ellei sinulla ole erityistä syytä suosia did:web:iä.", 74 + "externalDid": "Sinun did:web", 75 + "externalDidPlaceholder": "did:web:verkkotunnuksesi.fi", 76 + "externalDidHint": "Verkkotunnuksesi on tarjottava kelvollinen DID-dokumentti osoitteessa /.well-known/did.json, joka osoittaa tähän PDS:ään", 77 + "contactMethod": "Yhteysmenetelmä", 78 + "contactMethodHint": "Valitse, miten haluat vahvistaa tilisi ja vastaanottaa ilmoituksia. Tarvitset vain yhden.", 79 + "verificationMethod": "Vahvistusmenetelmä", 80 + "email": "Sähköposti", 81 + "emailAddress": "Sähköpostiosoite", 82 + "emailPlaceholder": "sinä@esimerkki.fi", 83 + "discord": "Discord", 84 + "discordId": "Discord-käyttäjätunnus", 85 + "discordIdPlaceholder": "Discord-käyttäjätunnuksesi", 86 + "discordIdHint": "Numeerinen Discord-käyttäjätunnuksesi (ota Kehittäjätila käyttöön löytääksesi sen)", 87 + "telegram": "Telegram", 88 + "telegramUsername": "Telegram-käyttäjänimi", 89 + "telegramUsernamePlaceholder": "@käyttäjänimesi", 90 + "signal": "Signal", 91 + "signalNumber": "Signal-puhelinnumero", 92 + "signalNumberPlaceholder": "+358401234567", 93 + "signalNumberHint": "Sisällytä maakoodi (esim. +358 Suomelle)", 94 + "inviteCode": "Kutsukoodi", 95 + "inviteCodePlaceholder": "Syötä kutsukoodisi", 96 + "inviteCodeRequired": "vaaditaan", 97 + "createButton": "Luo tili", 98 + "creating": "Luodaan tiliä...", 99 + "alreadyHaveAccount": "Onko sinulla jo tili?", 100 + "signIn": "Kirjaudu sisään", 101 + "wantPasswordless": "Haluatko salasanattoman turvallisuuden?", 102 + "createPasskeyAccount": "Luo pääsyavaintili", 103 + "validation": { 104 + "handleRequired": "Käyttäjänimi vaaditaan", 105 + "handleNoDots": "Käyttäjänimi ei voi sisältää pisteitä. Voit määrittää oman verkkotunnuksen tilin luomisen jälkeen.", 106 + "passwordRequired": "Salasana vaaditaan", 107 + "passwordLength": "Salasanan on oltava vähintään 8 merkkiä", 108 + "passwordsMismatch": "Salasanat eivät täsmää", 109 + "inviteCodeRequired": "Kutsukoodi vaaditaan", 110 + "externalDidRequired": "Ulkoinen did:web vaaditaan", 111 + "externalDidFormat": "Ulkoisen DID:n on alettava did:web:", 112 + "emailRequired": "Sähköposti vaaditaan sähköpostivahvistukseen", 113 + "discordIdRequired": "Discord-tunnus vaaditaan Discord-vahvistukseen", 114 + "telegramRequired": "Telegram-käyttäjänimi vaaditaan Telegram-vahvistukseen", 115 + "signalRequired": "Puhelinnumero vaaditaan Signal-vahvistukseen" 116 + } 117 + }, 118 + "dashboard": { 119 + "title": "Hallintapaneeli", 120 + "switchAccount": "Vaihda tiliä", 121 + "addAnotherAccount": "Lisää toinen tili", 122 + "signOut": "Kirjaudu ulos @{handle}", 123 + "deactivatedTitle": "Tili poistettu käytöstä", 124 + "deactivatedMessage": "Tilisi on tällä hetkellä poistettu käytöstä. Tämä tapahtuu yleensä tilin siirron aikana. Jotkut toiminnot voivat olla rajoitettuja, kunnes tilisi aktivoidaan uudelleen.", 125 + "accountOverview": "Tilin yleiskatsaus", 126 + "handle": "Käyttäjänimi", 127 + "did": "DID", 128 + "primaryContact": "Ensisijainen yhteystieto", 129 + "admin": "Ylläpitäjä", 130 + "deactivated": "Poistettu käytöstä", 131 + "verified": "Vahvistettu", 132 + "unverified": "Vahvistamaton", 133 + "navAppPasswords": "Sovellusten salasanat", 134 + "navAppPasswordsDesc": "Hallitse kolmannen osapuolen sovellusten salasanoja", 135 + "navSessions": "Aktiiviset istunnot", 136 + "navSessionsDesc": "Näytä ja hallitse kirjautumisistuntoja", 137 + "navInviteCodes": "Kutsukoodit", 138 + "navInviteCodesDesc": "Näytä ja luo kutsukoodeja", 139 + "navSettings": "Tilin asetukset", 140 + "navSettingsDesc": "Sähköposti, salasana, käyttäjänimi ja muuta", 141 + "navSecurity": "Turvallisuus", 142 + "navSecurityDesc": "Kaksivaiheinen tunnistautuminen", 143 + "navComms": "Viestintäasetukset", 144 + "navCommsDesc": "Discord-, Telegram-, Signal-kanavat", 145 + "navRepo": "Tietovarastoselaaja", 146 + "navRepoDesc": "Selaa ja hallitse raakoja AT Protocol -tietueita", 147 + "navAdmin": "Ylläpitopaneeli", 148 + "navAdminDesc": "Palvelintilastot ja ylläpitotoiminnot" 149 + }, 150 + "settings": { 151 + "title": "Tilin asetukset", 152 + "language": "Kieli", 153 + "languageDescription": "Valitse haluamasi kieli", 154 + "changeEmail": "Vaihda sähköposti", 155 + "currentEmail": "Nykyinen: {email}", 156 + "newEmail": "Uusi sähköposti", 157 + "newEmailPlaceholder": "uusi@esimerkki.fi", 158 + "changeEmailButton": "Vaihda sähköposti", 159 + "requesting": "Pyydetään...", 160 + "verificationCode": "Vahvistuskoodi", 161 + "verificationCodePlaceholder": "Syötä koodi sähköpostista", 162 + "confirmEmailChange": "Vahvista sähköpostin vaihto", 163 + "updating": "Päivitetään...", 164 + "changeHandle": "Vaihda käyttäjänimi", 165 + "currentHandle": "Nykyinen: @{handle}", 166 + "pdsHandle": "PDS-käyttäjänimi", 167 + "customDomain": "Oma verkkotunnus", 168 + "customDomainDescription": "Käytä omaa verkkotunnustasi käyttäjänimenä. Sinun on vahvistettava verkkotunnuksen omistajuus ensin.", 169 + "setupInstructions": "Asennusohjeet", 170 + "setupMethodsIntro": "Valitse jokin näistä vahvistusmenetelmistä:", 171 + "dnsMethod": "Vaihtoehto 1: DNS TXT -tietue (Suositellaan)", 172 + "dnsMethodDesc": "Lisää tämä TXT-tietue verkkotunnukseesi:", 173 + "httpMethod": "Vaihtoehto 2: HTTP Well-Known -tiedosto", 174 + "httpMethodDesc": "Tarjoa DID tästä URL-osoitteesta:", 175 + "httpMethodContent": "Tiedoston tulee sisältää vain:", 176 + "yourDomain": "Verkkotunnuksesi", 177 + "yourDomainPlaceholder": "esimerkki.fi", 178 + "verifyAndUpdate": "Vahvista ja päivitä käyttäjänimi", 179 + "verifying": "Vahvistetaan...", 180 + "newHandle": "Uusi käyttäjänimi", 181 + "newHandlePlaceholder": "käyttäjänimesi", 182 + "changeHandleButton": "Vaihda käyttäjänimi", 183 + "changePassword": "Vaihda salasana", 184 + "currentPassword": "Nykyinen salasana", 185 + "currentPasswordPlaceholder": "Syötä nykyinen salasana", 186 + "newPassword": "Uusi salasana", 187 + "newPasswordPlaceholder": "Vähintään 8 merkkiä", 188 + "confirmNewPassword": "Vahvista uusi salasana", 189 + "confirmNewPasswordPlaceholder": "Vahvista uusi salasana", 190 + "changePasswordButton": "Vaihda salasana", 191 + "changing": "Vaihdetaan...", 192 + "exportData": "Vie tiedot", 193 + "exportDataDescription": "Lataa koko tietovarastosi CAR-tiedostona (Content Addressable Archive). Tämä sisältää kaikki julkaisusi, tykkäyksesi, seuraamisesi ja muut tiedot.", 194 + "downloadRepo": "Lataa tietovarasto", 195 + "exporting": "Viedään...", 196 + "deleteAccount": "Poista tili", 197 + "deleteWarning": "Tämä toiminto on peruuttamaton. Kaikki tietosi poistetaan pysyvästi.", 198 + "requestDeletion": "Pyydä tilin poistoa", 199 + "confirmationCode": "Vahvistuskoodi (sähköpostista)", 200 + "confirmationCodePlaceholder": "Syötä vahvistuskoodi", 201 + "yourPassword": "Salasanasi", 202 + "yourPasswordPlaceholder": "Syötä salasanasi", 203 + "permanentlyDelete": "Poista tili pysyvästi", 204 + "deleting": "Poistetaan...", 205 + "messages": { 206 + "emailCodeSent": "Vahvistuskoodi lähetetty nykyiseen sähköpostiisi", 207 + "emailUpdated": "Sähköposti päivitetty", 208 + "handleUpdated": "Käyttäjänimi päivitetty", 209 + "passwordChanged": "Salasana vaihdettu", 210 + "passwordsMismatch": "Salasanat eivät täsmää", 211 + "passwordLength": "Salasanan on oltava vähintään 8 merkkiä", 212 + "deletionCodeSent": "Poistovahvistus lähetetty sähköpostiisi", 213 + "repoExported": "Tietovarasto viety", 214 + "confirmDelete": "Oletko täysin varma, että haluat poistaa tilisi? Tätä ei voi perua." 215 + } 216 + }, 217 + "appPasswords": { 218 + "title": "Sovellusten salasanat", 219 + "description": "Sovellusten salasanat mahdollistavat kirjautumisen kolmannen osapuolen sovelluksiin antamatta niille pääsalasanaasi. Jokainen sovellusen salasana voidaan perua erikseen.", 220 + "createNew": "Luo uusi sovelluksen salasana", 221 + "appNamePlaceholder": "Sovelluksen nimi (esim. Graysky, Skeets)", 222 + "created": "Sovelluksen salasana luotu", 223 + "createdMessage": "Kopioi tämä salasana nyt. Et voi nähdä sitä enää myöhemmin.", 224 + "yourPasswords": "Sovellustesi salasanat", 225 + "noPasswords": "Ei vielä sovellusten salasanoja", 226 + "revoke": "Peruuta", 227 + "revoking": "Peruutetaan...", 228 + "creating": "Luodaan...", 229 + "revokeConfirm": "Peruuta sovelluksen salasana \"{name}\"? Sovellukset, jotka käyttävät tätä salasanaa, eivät enää pääse tilillesi." 230 + }, 231 + "sessions": { 232 + "title": "Aktiiviset istunnot", 233 + "loadingSessions": "Ladataan istuntoja...", 234 + "noSessions": "Aktiivisia istuntoja ei löytynyt.", 235 + "current": "Nykyinen", 236 + "oauth": "OAuth", 237 + "session": "Istunto", 238 + "signOut": "Kirjaudu ulos", 239 + "revoke": "Peruuta", 240 + "revokeAll": "Peruuta kaikki muut istunnot", 241 + "revokeCurrentConfirm": "Tämä kirjaa sinut ulos tästä istunnosta. Jatketaanko?", 242 + "revokeConfirm": "Peruuta tämä istunto?", 243 + "revokeAllConfirm": "Tämä peruuttaa {count} muuta istuntoa. Jatketaanko?", 244 + "noOtherSessions": "Ei muita peruutettavia istuntoja", 245 + "failedToLoad": "Istuntojen lataaminen epäonnistui", 246 + "failedToRevoke": "Istunnon peruuttaminen epäonnistui", 247 + "failedToRevokeAll": "Istuntojen peruuttaminen epäonnistui", 248 + "created": "Luotu:", 249 + "expires": "Vanhenee:", 250 + "daysAgo": "{count} päivää sitten", 251 + "hoursAgo": "{count} tuntia sitten", 252 + "minutesAgo": "{count} minuuttia sitten", 253 + "justNow": "Juuri nyt" 254 + }, 255 + "inviteCodes": { 256 + "title": "Kutsukoodit", 257 + "description": "Kutsukoodit mahdollistavat ystävien kutsumisen. Jokainen koodi voidaan käyttää kerran.", 258 + "createNew": "Luo uusi kutsukoodi", 259 + "uses": "Käyttökerrat", 260 + "usesPlaceholder": "Käyttökertojen määrä (1-100)", 261 + "yourCodes": "Kutsukoodisi", 262 + "noCodes": "Ei vielä kutsukoodeja", 263 + "available": "Saatavilla", 264 + "used": "Käyttänyt @{handle}", 265 + "disabled": "Poistettu käytöstä", 266 + "usedBy": "Käyttänyt", 267 + "creating": "Luodaan...", 268 + "disableConfirm": "Poista tämä kutsukoodi käytöstä? Sitä ei voi enää käyttää.", 269 + "created": "Kutsukoodi luotu", 270 + "copy": "Kopioi", 271 + "createdOn": "Luotu {date}" 272 + }, 273 + "security": { 274 + "title": "Turvallisuus", 275 + "passkeys": "Pääsyavaimet", 276 + "passkeysDescription": "Pääsyavaimet tarjoavat turvallisen, salasanattoman tunnistautumisen käyttäen laitteesi sisäänrakennettua turvallisuutta (sormenjälki, kasvot tai PIN).", 277 + "addPasskey": "Lisää pääsyavain", 278 + "adding": "Lisätään...", 279 + "noPasskeys": "Ei rekisteröityjä pääsyavaimia", 280 + "passkeyName": "Pääsyavaimen nimi", 281 + "passkeyNamePlaceholder": "esim. MacBook Pro, iPhone", 282 + "register": "Rekisteröi", 283 + "registering": "Rekisteröidään...", 284 + "rename": "Nimeä uudelleen", 285 + "renaming": "Nimetään uudelleen...", 286 + "deletePasskey": "Poista", 287 + "deletePasskeyConfirm": "Poista pääsyavain \"{name}\"? Et voi enää käyttää sitä kirjautumiseen.", 288 + "totp": "Todentajasovellus (TOTP)", 289 + "totpDescription": "Käytä todentajasovellusta kuten Google Authenticator, Authy tai 1Password kaksivaiheiseen tunnistautumiseen.", 290 + "totpEnabled": "TOTP on käytössä", 291 + "totpDisabled": "TOTP ei ole käytössä", 292 + "enableTotp": "Ota TOTP käyttöön", 293 + "disableTotp": "Poista TOTP käytöstä", 294 + "disabling": "Poistetaan käytöstä...", 295 + "totpSetup": "Määritä todentajasovellus", 296 + "totpSetupInstructions": "Skannaa tämä QR-koodi todentajasovelluksellasi ja syötä sitten 6-numeroinen koodi vahvistaaksesi.", 297 + "totpCode": "Vahvistuskoodi", 298 + "totpCodePlaceholder": "Syötä 6-numeroinen koodi", 299 + "verifyAndEnable": "Vahvista ja ota käyttöön", 300 + "backupCodes": "Varakoodit", 301 + "backupCodesDescription": "Käytä näitä koodeja kirjautuaksesi sisään, jos menetät pääsyn todentajasovellukseesi. Jokainen koodi voidaan käyttää vain kerran.", 302 + "regenerateBackupCodes": "Luo uudet varakoodit", 303 + "regenerating": "Luodaan...", 304 + "regenerateConfirm": "Luo uudet varakoodit? Nykyiset koodisi eivät enää toimi.", 305 + "legacyLogin": "Vanhentunut kirjautuminen", 306 + "legacyLoginDescription": "Salli kirjautuminen käyttäjänimellä/salasanalla suoraan (vanhentunut tila). Kun tämä on poistettu käytöstä, sinun on käytettävä OAuthia MFA:n kanssa.", 307 + "legacyLoginOn": "Vanhentunut kirjautuminen on käytössä", 308 + "legacyLoginOff": "Vanhentunut kirjautuminen on poistettu käytöstä", 309 + "enableLegacyLogin": "Ota vanhentunut kirjautuminen käyttöön", 310 + "disableLegacyLogin": "Poista vanhentunut kirjautuminen käytöstä", 311 + "legacyLoginWarning": "Varoitus: Vanhentuneen kirjautumisen käyttöönotto ohittaa MFA:n suorissa salasanakirjautumisissa. Ota käyttöön vain jos sovellusyhteensopivuus sitä vaatii.", 312 + "totpPasswordWarning": "Kun TOTP on käytössä, salasanan vaihtaminen Bluesky-sovelluksesta (tai muista vanhentuneista sovelluksista) estetään. Salasanan vaihtamiseen on kaksi vaihtoehtoa:", 313 + "totpPasswordOption1Label": "Vaihda se täällä:", 314 + "totpPasswordOption1Text": "Käytä tämän sivuston", 315 + "totpPasswordOption1Link": "Asetukset-sivua", 316 + "totpPasswordOption1Suffix": "jossa voit vahvistaa todentajasovelluksellasi.", 317 + "totpPasswordOption2Label": "Vahvista istuntosi ensin:", 318 + "totpPasswordOption2Text": "Käytä", 319 + "totpPasswordOption2Link": "uudelleentodennusvaihtoehtoa", 320 + "totpPasswordOption2Suffix": "vahvistaaksesi Bluesky-istuntosi TOTP:lla, sitten salasanan vaihto toimii väliaikaisesti.", 321 + "legacyAppsTitle": "Mitä ovat vanhentuneet sovellukset?", 322 + "legacyAppsDescription": "Jotkin sovellukset (kuten virallinen Bluesky-sovellus) käyttävät vanhentunutta todennusta, joka vaatii vain salasanasi. Kun sinulla on MFA käytössä, nämä sovellukset ohittavat toisen tekijäsi. Vanhentuneen kirjautumisen poistaminen käytöstä pakottaa kaikki sovellukset käyttämään OAuthia, joka soveltaa MFA:ta oikein.", 323 + "password": "Salasana", 324 + "passwordStatus": "Sinulla on salasana asetettuna", 325 + "noPassword": "Ei salasanaa asetettuna (vain pääsyavaintili)", 326 + "setPassword": "Aseta salasana", 327 + "removePassword": "Poista salasana", 328 + "removePasswordConfirm": "Poista salasanasi? Sinun on käytettävä pääsyavaimia kirjautuaksesi sisään.", 329 + "removing": "Poistetaan...", 330 + "loading": "Ladataan...", 331 + "loadingPasskeys": "Ladataan pääsyavaimia...", 332 + "cancel": "Peruuta", 333 + "save": "Tallenna", 334 + "back": "Takaisin", 335 + "next": "Seuraava: Vahvista koodi", 336 + "copyToClipboard": "Kopioi leikepöydälle", 337 + "savedMyCodes": "Olen tallentanut koodini", 338 + "cantScan": "Etkö voi skannata? Syötä manuaalisesti", 339 + "unnamedPasskey": "Nimetön pääsyavain", 340 + "added": "Lisätty", 341 + "lastUsed": "Viimeksi käytetty", 342 + "passwordDescription": "Hallitse tilisi salasanaa. Jos sinulla on pääsyavaimia määritettynä, voit halutessasi poistaa salasanasi täysin salasanattoman kokemuksen saavuttamiseksi.", 343 + "disableTotpWarning": "Tämä tekee tilistäsi vähemmän turvallisen.", 344 + "removePasswordWarning": "Tämä tekee tilistäsi vain pääsyavaintilin. Voit kirjautua sisään vain rekisteröidyillä pääsyavaimillasi. Jos menetät pääsyn kaikkiin pääsyavaimeesi, voit palauttaa tilisi ilmoituskanavan kautta.", 345 + "beforeProceeding": "Ennen kuin jatkat:", 346 + "beforeProceedingItem1": "Varmista, että sinulla on vähintään yksi luotettava pääsyavain rekisteröitynä", 347 + "beforeProceedingItem2": "Harkitse pääsyavainten rekisteröimistä useille laitteille", 348 + "beforeProceedingItem3": "Varmista, että palautusilmoituskanavasi on ajan tasalla", 349 + "addPasskeyFirst": "Lisää vähintään yksi pääsyavain ennen kuin voit poistaa salasanasi.", 350 + "passkeyOnlyHint": "Kirjaudut sisään vain pääsyavaimilla. Jos menetät pääsyn pääsyavaimeesi, voit palauttaa tilisi käyttämällä \"Kadotitko pääsyavaimen?\" -linkkiä kirjautumissivulla.", 351 + "trustedDevices": "Luotetut laitteet", 352 + "trustedDevicesDescription": "Hallitse laitteita, jotka voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.", 353 + "manageTrustedDevices": "Hallitse luotettuja laitteita", 354 + "appCompatibility": "Sovellusyhteensopivuus", 355 + "enterPassword": "Syötä salasanasi", 356 + "legacyLoginEnabled": "Vanhentuneiden sovellusten kirjautuminen käytössä", 357 + "legacyLoginDisabled": "Vanhentuneiden sovellusten kirjautuminen poistettu käytöstä - vain OAuth-sovellukset voivat kirjautua", 358 + "failedToUpdatePreference": "Asetuksen päivittäminen epäonnistui", 359 + "passwordRemoved": "Salasana poistettu. Tilisi on nyt vain pääsyavaintili.", 360 + "failedToRemovePassword": "Salasanan poistaminen epäonnistui", 361 + "failedToLoadTotpStatus": "TOTP-tilan lataaminen epäonnistui", 362 + "totpEnabledSuccess": "Kaksivaiheinen tunnistautuminen käytössä", 363 + "totpDisabledSuccess": "Kaksivaiheinen tunnistautuminen poistettu käytöstä", 364 + "backupCodesCopied": "Varakoodit kopioitu leikepöydälle", 365 + "failedToLoadPasskeys": "Pääsyavainten lataaminen epäonnistui", 366 + "passkeysNotSupported": "Pääsyavaimia ei tueta tässä selaimessa", 367 + "passkeyCreationCancelled": "Pääsyavaimen luominen peruutettu", 368 + "passkeyAddedSuccess": "Pääsyavain lisätty", 369 + "passkeyDeleted": "Pääsyavain poistettu", 370 + "passkeyRenamed": "Pääsyavain nimetty uudelleen" 371 + }, 372 + "comms": { 373 + "title": "Viestintäasetukset", 374 + "description": "Valitse, miten haluat vastaanottaa tärkeitä viestejä kuten salasanan palautuksia, turvallisuushälytyksiä ja tilipäivityksiä.", 375 + "preferredChannel": "Ensisijainen kanava", 376 + "preferredChannelDescription": "Valitse ensisijainen tapasi vastaanottaa viestejä. Sinun on määritettävä kanava ennen kuin voit valita sen.", 377 + "channelConfiguration": "Kanavan määritys", 378 + "emailVia": "Vastaanota viestejä sähköpostitse", 379 + "discordVia": "Vastaanota viestejä Discord-yksityisviestinä", 380 + "telegramVia": "Vastaanota viestejä Telegramissa", 381 + "signalVia": "Vastaanota viestejä Signalissa", 382 + "configureToEnable": "Määritä alla ottaaksesi käyttöön", 383 + "emailManagedInSettings": "Sähköpostisi hallinnoidaan Tilin asetuksissa", 384 + "discordIdHint": "Discord-käyttäjätunnuksesi (ei käyttäjänimi). Ota Kehittäjätila käyttöön Discordissa kopioidaksesi sen.", 385 + "telegramHint": "Telegram-käyttäjänimesi ilman @-merkkiä", 386 + "signalHint": "Signal-puhelinnumerosi maakoodilla", 387 + "primary": "Ensisijainen", 388 + "verified": "Vahvistettu", 389 + "notVerified": "Vahvistamaton", 390 + "verifyButton": "Vahvista", 391 + "verifyCodePlaceholder": "Syötä vahvistuskoodi", 392 + "submit": "Lähetä", 393 + "saving": "Tallennetaan...", 394 + "savePreferences": "Tallenna asetukset", 395 + "preferencesSaved": "Viestintäasetukset tallennettu", 396 + "verifiedSuccess": "{channel} vahvistettu", 397 + "messageHistory": "Viestihistoria", 398 + "historyDescription": "Näytä viimeisimmät tilillesi lähetetyt viestit.", 399 + "loadHistory": "Lataa historia", 400 + "hideHistory": "Piilota historia", 401 + "noMessages": "Viestejä ei löytynyt.", 402 + "sent": "lähetetty", 403 + "failed": "epäonnistui" 404 + }, 405 + "repoExplorer": { 406 + "title": "Tietovarastoselaaja", 407 + "description": "Selaa ja hallitse raakoja AT Protocol -tietueitasi.", 408 + "collections": "Kokoelmat", 409 + "noCollections": "Kokoelmia ei löytynyt", 410 + "records": "Tietueet", 411 + "noRecords": "Ei tietueita tässä kokoelmassa", 412 + "recordDetails": "Tietueen tiedot", 413 + "rkey": "Tietueavain", 414 + "cid": "CID", 415 + "value": "Arvo", 416 + "deleteRecord": "Poista tietue", 417 + "deleteConfirm": "Poista tietue {rkey}? Tätä ei voi perua.", 418 + "unknownError": "Tuntematon virhe tapahtui", 419 + "invalidJson": "Virheellinen JSON", 420 + "collectionRequired": "Kokoelma vaaditaan", 421 + "recordCreated": "Tietue luotu: {uri}", 422 + "recordUpdated": "Tietue päivitetty", 423 + "recordDeleted": "Tietue poistettu", 424 + "newRecord": "Uusi tietue", 425 + "createRecord": "Luo tietue", 426 + "filterCollections": "Suodata kokoelmia...", 427 + "filterRecords": "Suodata tietueita...", 428 + "noCollectionsYet": "Ei vielä kokoelmia. Luo ensimmäinen tietueesi aloittaaksesi.", 429 + "loadMore": "Lataa lisää", 430 + "recordJson": "Tietueen JSON", 431 + "saving": "Tallennetaan...", 432 + "updateRecord": "Päivitä tietue", 433 + "collectionNsid": "Kokoelma (NSID)", 434 + "recordKeyOptional": "Tietueavain (valinnainen)", 435 + "autoGenerated": "Luodaan automaattisesti jos tyhjä (TID)", 436 + "autoGeneratedHint": "Jätä tyhjäksi luodaksesi TID-pohjaisen avaimen automaattisesti", 437 + "creating": "Luodaan...", 438 + "demoPostText": "Hei PDS:ltäni! Tämä on ensimmäinen julkaisuni.", 439 + "demoDisplayName": "Näyttönimesi", 440 + "demoBio": "Lyhyt kuvaus itsestäsi." 441 + }, 442 + "admin": { 443 + "title": "Ylläpitopaneeli", 444 + "serverStats": "Palvelintilastot", 445 + "users": "Käyttäjät", 446 + "repos": "Tietovarastot", 447 + "records": "Tietueet", 448 + "blobStorage": "Blob-tallennustila", 449 + "refreshStats": "Päivitä tilastot", 450 + "userManagement": "Käyttäjähallinta", 451 + "searchPlaceholder": "Hae käyttäjänimellä (valinnainen)", 452 + "searchUsers": "Hae käyttäjiä", 453 + "noUsers": "Käyttäjiä ei löytynyt", 454 + "handle": "Käyttäjänimi", 455 + "email": "Sähköposti", 456 + "status": "Tila", 457 + "created": "Luotu", 458 + "loadMore": "Lataa lisää", 459 + "inviteCodes": "Kutsukoodit", 460 + "loadInviteCodes": "Lataa kutsukoodit", 461 + "refresh": "Päivitä", 462 + "noInvites": "Kutsukoodeja ei löytynyt", 463 + "code": "Koodi", 464 + "available": "Saatavilla", 465 + "uses": "Käyttökerrat", 466 + "actions": "Toiminnot", 467 + "disable": "Poista käytöstä", 468 + "disableInviteConfirm": "Poista kutsukoodi {code} käytöstä?", 469 + "active": "Aktiivinen", 470 + "exhausted": "Käytetty loppuun", 471 + "disabled": "Poistettu käytöstä", 472 + "userDetails": "Käyttäjän tiedot", 473 + "did": "DID", 474 + "invites": "Kutsut", 475 + "enabled": "Käytössä", 476 + "enableInvites": "Ota kutsut käyttöön", 477 + "disableInvites": "Poista kutsut käytöstä", 478 + "deleteAccount": "Poista tili", 479 + "deleteConfirm": "Poista tili @{handle}? Tätä ei voi perua.", 480 + "verified": "Vahvistettu", 481 + "unverified": "Vahvistamaton", 482 + "deactivated": "Poistettu käytöstä" 483 + }, 484 + "oauth": { 485 + "login": { 486 + "title": "Kirjaudu sisään", 487 + "subtitle": "Kirjaudu sisään jatkaaksesi sovellukseen", 488 + "signingIn": "Kirjaudutaan...", 489 + "authenticating": "Todennetaan...", 490 + "checkingPasskey": "Tarkistetaan pääsyavainta...", 491 + "signInWithPasskey": "Kirjaudu pääsyavaimella", 492 + "passkeyNotSetUp": "Pääsyavainta ei ole määritetty", 493 + "orUsePassword": "tai käytä salasanaa", 494 + "password": "Salasana", 495 + "rememberDevice": "Muista tämä laite", 496 + "passkeyHintChecking": "Tarkistetaan pääsyavaimen tilaa...", 497 + "passkeyHintAvailable": "Kirjaudu pääsyavaimellasi", 498 + "passkeyHintNotAvailable": "Ei rekisteröityjä pääsyavaimia tälle tilille" 499 + }, 500 + "consent": { 501 + "title": "Valtuuta sovellus", 502 + "appWantsAccess": "{app} haluaa käyttää tiliäsi", 503 + "permissions": "Tämä sovellus voi:", 504 + "readProfile": "Lukea profiilitietosi", 505 + "readPosts": "Lukea julkaisusi ja sisältösi", 506 + "writePosts": "Luoda ja poistaa julkaisuja puolestasi", 507 + "readNotifications": "Lukea ilmoituksesi", 508 + "fullAccess": "Täysi pääsy tiliisi", 509 + "authorize": "Valtuuta", 510 + "deny": "Estä", 511 + "authorizing": "Valtuutetaan...", 512 + "rememberChoice": "Muista tämä valinta", 513 + "signingInAs": "Kirjaudutaan käyttäjänä:", 514 + "permissionsRequested": "Pyydetyt oikeudet", 515 + "required": "Vaaditaan", 516 + "rememberChoiceLabel": "Muista valintani tälle sovellukselle" 517 + }, 518 + "accounts": { 519 + "title": "Valitse tili", 520 + "subtitle": "Valitse tili jatkaaksesi", 521 + "useAnother": "Käytä toista tiliä" 522 + }, 523 + "twoFactor": { 524 + "title": "Kaksivaiheinen tunnistautuminen", 525 + "subtitle": "Lisävahvistus vaaditaan", 526 + "usePasskey": "Käytä pääsyavainta", 527 + "useTotp": "Käytä todentajasovellusta", 528 + "verifying": "Vahvistetaan..." 529 + }, 530 + "twoFactorCode": { 531 + "title": "Kaksivaiheinen tunnistautuminen", 532 + "subtitle": "Vahvistuskoodi on lähetetty {channel}. Syötä koodi alla jatkaaksesi.", 533 + "codeLabel": "Vahvistuskoodi", 534 + "codePlaceholder": "Syötä 6-numeroinen koodi", 535 + "verify": "Vahvista", 536 + "verifying": "Vahvistetaan...", 537 + "errors": { 538 + "missingRequestUri": "Puuttuva request_uri-parametri", 539 + "verificationFailed": "Vahvistus epäonnistui", 540 + "connectionFailed": "Palvelimeen yhdistäminen epäonnistui", 541 + "unexpectedResponse": "Odottamaton vastaus palvelimelta" 542 + } 543 + }, 544 + "totp": { 545 + "title": "Syötä todentajakoodi", 546 + "subtitle": "Syötä 6-numeroinen koodi todentajasovelluksestasi", 547 + "codePlaceholder": "Syötä 6-numeroinen koodi", 548 + "verify": "Vahvista", 549 + "verifying": "Vahvistetaan...", 550 + "useBackupCode": "Käytä varakoodia sen sijaan", 551 + "backupCodePlaceholder": "Syötä varakoodi", 552 + "trustDevice": "Luota tähän laitteeseen 30 päivää", 553 + "hintBackupCode": "Käytetään varakoodia", 554 + "hintTotpCode": "Käytetään todentajakoodia", 555 + "hintDefault": "6 numeroa todentajalle, 8 merkkiä varakoodille" 556 + }, 557 + "passkey": { 558 + "title": "Pääsyavaimen vahvistus", 559 + "subtitle": "Käytä pääsyavaintasi vahvistaaksesi henkilöllisyytesi", 560 + "waiting": "Odotetaan pääsyavainta...", 561 + "useTotp": "Käytä todentajasovellusta sen sijaan" 562 + }, 563 + "error": { 564 + "title": "Valtuutusvirhe", 565 + "genericError": "Valtuutuksen aikana tapahtui virhe.", 566 + "tryAgain": "Yritä uudelleen", 567 + "backToApp": "Takaisin sovellukseen" 568 + } 569 + }, 570 + "verify": { 571 + "title": "Vahvista tilisi", 572 + "subtitle": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä se alla viimeistelläksesi rekisteröinnin.", 573 + "codePlaceholder": "Syötä 6-numeroinen koodi", 574 + "codeLabel": "Vahvistuskoodi", 575 + "verifyButton": "Vahvista tili", 576 + "verifying": "Vahvistetaan...", 577 + "resendCode": "Lähetä koodi uudelleen", 578 + "resending": "Lähetetään uudelleen...", 579 + "codeResent": "Vahvistuskoodi lähetetty uudelleen!", 580 + "backToLogin": "Takaisin kirjautumiseen", 581 + "verifyingAccount": "Vahvistetaan tiliä: @{handle}", 582 + "startOver": "Aloita alusta toisella tilillä", 583 + "noPending": "Odottavaa vahvistusta ei löytynyt.", 584 + "noPendingInfo": "Jos loit tilin äskettäin ja sinun on vahvistettava se, sinun on ehkä luotava uusi tili. Jos olet jo vahvistanut tilisi, voit kirjautua sisään.", 585 + "createAccount": "Luo tili", 586 + "signIn": "Kirjaudu sisään" 587 + }, 588 + "resetPassword": { 589 + "title": "Palauta salasana", 590 + "forgotTitle": "Unohtuiko salasana", 591 + "subtitle": "Syötä saamasi koodi ja valitse uusi salasana.", 592 + "forgotSubtitle": "Syötä käyttäjänimesi tai sähköpostisi, niin lähetämme sinulle koodin salasanan palauttamiseksi.", 593 + "handleOrEmail": "Käyttäjänimi tai sähköposti", 594 + "emailPlaceholder": "käyttäjänimi tai sinä@esimerkki.fi", 595 + "sendCode": "Lähetä palautuskoodi", 596 + "sending": "Lähetetään...", 597 + "codeSent": "Palautuskoodi lähetetty! Tarkista ensisijainen ilmoituskanavasi.", 598 + "enterCode": "Syötä koodi sähköpostistasi ja uusi salasanasi.", 599 + "code": "Palautuskoodi", 600 + "codePlaceholder": "Syötä palautuskoodi", 601 + "newPassword": "Uusi salasana", 602 + "newPasswordPlaceholder": "Vähintään 8 merkkiä", 603 + "confirmPassword": "Vahvista salasana", 604 + "confirmPasswordPlaceholder": "Vahvista uusi salasana", 605 + "resetButton": "Palauta salasana", 606 + "resetting": "Palautetaan...", 607 + "success": "Salasana palautettu!", 608 + "backToLogin": "Takaisin kirjautumiseen", 609 + "requestNewCode": "Pyydä uusi koodi", 610 + "passwordsMismatch": "Salasanat eivät täsmää", 611 + "passwordLength": "Salasanan on oltava vähintään 8 merkkiä" 612 + }, 613 + "recoverPasskey": { 614 + "title": "Palauta tilisi", 615 + "invalidLinkTitle": "Virheellinen palautuslinkki", 616 + "invalidLinkMessage": "Tämä palautuslinkki on virheellinen tai vioittunut. Pyydä uusi palautussähköposti.", 617 + "goToLogin": "Siirry kirjautumiseen", 618 + "successTitle": "Salasana asetettu!", 619 + "successMessage": "Väliaikainen salasanasi on asetettu. Voit nyt kirjautua sisään tällä salasanalla.", 620 + "successNextSteps": "Kirjautumisen jälkeen suosittelemme lisäämään uuden pääsyavaimen turvallisuusasetuksissasi palauttaaksesi vain pääsyavaintodennuksen.", 621 + "signIn": "Kirjaudu sisään", 622 + "subtitle": "Aseta väliaikainen salasana saadaksesi pääsyn takaisin vain pääsyavaintiliisi.", 623 + "newPassword": "Uusi salasana", 624 + "newPasswordPlaceholder": "Vähintään 8 merkkiä", 625 + "confirmPassword": "Vahvista salasana", 626 + "confirmPasswordPlaceholder": "Vahvista salasanasi", 627 + "whatHappensNext": "Mitä tapahtuu seuraavaksi?", 628 + "whatHappensNextDetail": "Tämän salasanan asettamisen jälkeen voit kirjautua sisään ja lisätä uuden pääsyavaimen turvallisuusasetuksissasi. Kun sinulla on uusi pääsyavain, voit halutessasi poistaa väliaikaisen salasanan.", 629 + "setPassword": "Aseta salasana", 630 + "settingPassword": "Asetetaan salasanaa...", 631 + "validation": { 632 + "passwordRequired": "Uusi salasana vaaditaan", 633 + "passwordLength": "Salasanan on oltava vähintään 8 merkkiä", 634 + "passwordsMismatch": "Salasanat eivät täsmää" 635 + }, 636 + "errors": { 637 + "invalidLink": "Virheellinen palautuslinkki. Pyydä uusi.", 638 + "expired": "Tämä palautuslinkki on vanhentunut. Pyydä uusi." 639 + } 640 + }, 641 + "requestPasskeyRecovery": { 642 + "title": "Palauta pääsyavaintili", 643 + "subtitle": "Menetit pääsyn pääsyavaimeesi? Syötä käyttäjänimesi tai sähköpostisi, niin lähetämme sinulle palautuslinkin.", 644 + "successTitle": "Palautuslinkki lähetetty", 645 + "successMessage": "Jos tilisi on olemassa ja on vain pääsyavaintili, saat palautuslinkin ensisijaiseen ilmoituskanavaasi.", 646 + "successInfo": "Linkki vanhenee tunnin kuluttua. Tarkista sähköpostisi, Discord, Telegram tai Signal tilisi asetusten mukaan.", 647 + "handleOrEmail": "Käyttäjänimi tai sähköposti", 648 + "emailPlaceholder": "käyttäjänimi tai sinä@esimerkki.fi", 649 + "howItWorks": "Miten se toimii", 650 + "howItWorksDetail": "Lähetämme suojatun linkin rekisteröityyn ilmoituskanavaasi. Klikkaa linkkiä asettaaksesi väliaikaisen salasanan. Sitten voit kirjautua sisään ja lisätä uuden pääsyavaimen.", 651 + "sendRecoveryLink": "Lähetä palautuslinkki", 652 + "sending": "Lähetetään...", 653 + "backToLogin": "Takaisin kirjautumiseen" 654 + }, 655 + "registerPasskey": { 656 + "title": "Luo pääsyavaintili", 657 + "subtitle": "Luo salasanaton tili pääsyavaimella.", 658 + "handle": "Käyttäjänimi", 659 + "handlePlaceholder": "nimesi", 660 + "handleHint": "Täydellinen käyttäjänimesi on: @{handle}", 661 + "email": "Sähköpostiosoite", 662 + "emailPlaceholder": "sinä@esimerkki.fi", 663 + "inviteCode": "Kutsukoodi", 664 + "inviteCodePlaceholder": "Syötä kutsukoodisi", 665 + "createButton": "Luo tili", 666 + "creating": "Luodaan...", 667 + "alreadyHaveAccount": "Onko sinulla jo tili?", 668 + "signIn": "Kirjaudu sisään", 669 + "wantPassword": "Haluatko käyttää salasanaa?", 670 + "createPasswordAccount": "Luo salasanatili" 671 + }, 672 + "trustedDevices": { 673 + "title": "Luotetut laitteet", 674 + "backToSecurity": "← Turvallisuusasetukset", 675 + "description": "Luotetut laitteet voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.", 676 + "noDevices": "Ei vielä luotettuja laitteita.", 677 + "noDevicesHint": "Kun kirjaudut sisään kaksivaiheisen tunnistautumisen ollessa käytössä, voit valita luottaa laitteeseen 30 päivää.", 678 + "lastSeen": "Viimeksi nähty:", 679 + "trustedSince": "Luotettu alkaen:", 680 + "trustExpires": "Luottamus vanhenee:", 681 + "expired": "Vanhentunut", 682 + "tomorrow": "Huomenna", 683 + "inDays": "{days} päivän kuluttua", 684 + "revoke": "Peruuta luottamus", 685 + "revokeConfirm": "Oletko varma, että haluat peruuttaa luottamuksen tälle laitteelle? Sinun on syötettävä 2FA-koodisi seuraavan kerran kirjautuessasi tältä laitteelta.", 686 + "deviceRevoked": "Laitteen luottamus peruutettu", 687 + "deviceRenamed": "Laite nimetty uudelleen", 688 + "deviceNamePlaceholder": "Laitteen nimi", 689 + "browser": "Selain:", 690 + "unknownDevice": "Tuntematon laite" 691 + }, 692 + "reauth": { 693 + "title": "Uudelleentodennus vaaditaan", 694 + "subtitle": "Vahvista henkilöllisyytesi jatkaaksesi.", 695 + "usePassword": "Käytä salasanaa", 696 + "usePasskey": "Käytä pääsyavainta", 697 + "useTotp": "Käytä todentajaa", 698 + "passwordPlaceholder": "Syötä salasanasi", 699 + "totpPlaceholder": "Syötä 6-numeroinen koodi", 700 + "verify": "Vahvista", 701 + "verifying": "Vahvistetaan...", 702 + "cancel": "Peruuta" 703 + } 704 + }
+2
frontend/src/locales/ja.json
··· 306 306 "legacyLoginDescription": "ユーザー名/パスワードでの直接ログイン(レガシーモード)を許可します。無効にすると、MFA 付きの OAuth を使用する必要があります。", 307 307 "legacyLoginOn": "レガシーログインは有効です", 308 308 "legacyLoginOff": "レガシーログインは無効です", 309 + "enableLegacyLogin": "レガシーログインを有効にする", 310 + "disableLegacyLogin": "レガシーログインを無効にする", 309 311 "legacyLoginWarning": "警告: レガシーログインを有効にすると、直接パスワードログインの MFA がバイパスされます。アプリの互換性が必要な場合にのみ有効にしてください。", 310 312 "totpPasswordWarning": "TOTP が有効な場合、Bluesky アプリ(または他のレガシーアプリ)からパスワードを変更することはできません。パスワードを変更するには、2つの方法があります:", 311 313 "totpPasswordOption1Label": "ここで変更する:",
+2
frontend/src/locales/ko.json
··· 306 306 "legacyLoginDescription": "사용자 이름/비밀번호로 직접 로그인(레거시 모드)을 허용합니다. 비활성화하면 MFA가 있는 OAuth를 사용해야 합니다.", 307 307 "legacyLoginOn": "레거시 로그인이 활성화되었습니다", 308 308 "legacyLoginOff": "레거시 로그인이 비활성화되었습니다", 309 + "enableLegacyLogin": "레거시 로그인 활성화", 310 + "disableLegacyLogin": "레거시 로그인 비활성화", 309 311 "legacyLoginWarning": "경고: 레거시 로그인을 활성화하면 직접 비밀번호 로그인에 대한 MFA가 우회됩니다. 앱 호환성이 필요한 경우에만 활성화하세요.", 310 312 "totpPasswordWarning": "TOTP가 활성화되면 Bluesky 앱(또는 기타 레거시 앱)에서 비밀번호를 변경할 수 없습니다. 비밀번호를 변경하려면 두 가지 방법이 있습니다:", 311 313 "totpPasswordOption1Label": "여기에서 변경:",
+704
frontend/src/locales/sv.json
··· 1 + { 2 + "common": { 3 + "loading": "Laddar...", 4 + "error": "Fel", 5 + "save": "Spara", 6 + "cancel": "Avbryt", 7 + "back": "Tillbaka", 8 + "done": "Klar", 9 + "refresh": "Uppdatera", 10 + "create": "Skapa", 11 + "delete": "Radera", 12 + "confirm": "Bekräfta", 13 + "created": "Skapad", 14 + "expires": "Upphör", 15 + "name": "Namn", 16 + "dashboard": "Kontrollpanel", 17 + "backToDashboard": "← Kontrollpanel" 18 + }, 19 + "login": { 20 + "title": "Logga in", 21 + "subtitle": "Logga in för att hantera ditt PDS-konto", 22 + "button": "Logga in", 23 + "redirecting": "Omdirigerar...", 24 + "chooseAccount": "Välj ett konto", 25 + "signInToAnother": "Logga in med ett annat konto", 26 + "backToSaved": "← Tillbaka till sparade konton", 27 + "forgotPassword": "Glömt lösenordet?", 28 + "lostPasskey": "Tappat bort nyckeln?", 29 + "noAccount": "Har du inget konto?", 30 + "createAccount": "Skapa konto", 31 + "removeAccount": "Ta bort från sparade konton" 32 + }, 33 + "verification": { 34 + "title": "Verifiera ditt konto", 35 + "subtitle": "Ditt konto behöver verifieras. Ange koden som skickades till din verifieringsmetod.", 36 + "codeLabel": "Verifieringskod", 37 + "codePlaceholder": "Ange 6-siffrig kod", 38 + "verifyButton": "Verifiera konto", 39 + "verifying": "Verifierar...", 40 + "resendButton": "Skicka kod igen", 41 + "resending": "Skickar igen...", 42 + "resent": "Verifieringskod skickad igen!", 43 + "backToLogin": "Tillbaka till inloggning" 44 + }, 45 + "register": { 46 + "title": "Skapa konto", 47 + "subtitle": "Skapa ett nytt konto på denna PDS", 48 + "handle": "Användarnamn", 49 + "handlePlaceholder": "dittnamn", 50 + "handleHint": "Ditt fullständiga användarnamn blir: @{handle}", 51 + "handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats i Inställningar.", 52 + "password": "Lösenord", 53 + "passwordPlaceholder": "Minst 8 tecken", 54 + "confirmPassword": "Bekräfta lösenord", 55 + "confirmPasswordPlaceholder": "Bekräfta ditt lösenord", 56 + "identityType": "Identitetstyp", 57 + "identityHint": "Välj hur din decentraliserade identitet ska hanteras.", 58 + "didPlc": "did:plc", 59 + "didPlcRecommended": "(Rekommenderas)", 60 + "didPlcHint": "Portabel identitet hanterad av PLC Directory", 61 + "didWeb": "did:web", 62 + "didWebHint": "Identitet lagrad på denna PDS (läs varningen nedan)", 63 + "didWebBYOD": "did:web (egen domän)", 64 + "didWebBYODHint": "Använd din egen domän", 65 + "didWebWarningTitle": "Viktigt: Förstå avvägningarna", 66 + "didWebWarning1": "Permanent koppling till denna PDS:", 67 + "didWebWarning1Detail": "Din identitet blir {did}. Även om du flyttar till en annan PDS senare måste denna server fortsätta att vara värd för ditt DID-dokument.", 68 + "didWebWarning2": "Ingen återställningsmekanism:", 69 + "didWebWarning2Detail": "Till skillnad från did:plc har did:web inga rotationsnycklar. Om denna PDS går offline permanent kan din identitet inte återställas.", 70 + "didWebWarning3": "Vi förbinder oss till dig:", 71 + "didWebWarning3Detail": "Om du flyttar härifrån kommer vi att fortsätta tillhandahålla ett minimalt DID-dokument som pekar på din nya PDS. Din identitet förblir funktionell.", 72 + "didWebWarning4": "Rekommendation:", 73 + "didWebWarning4Detail": "Välj did:plc om du inte har en specifik anledning att föredra did:web.", 74 + "externalDid": "Din did:web", 75 + "externalDidPlaceholder": "did:web:dindomän.se", 76 + "externalDidHint": "Din domän måste tillhandahålla ett giltigt DID-dokument på /.well-known/did.json som pekar på denna PDS", 77 + "contactMethod": "Kontaktmetod", 78 + "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot notiser. Du behöver bara en.", 79 + "verificationMethod": "Verifieringsmetod", 80 + "email": "E-post", 81 + "emailAddress": "E-postadress", 82 + "emailPlaceholder": "du@exempel.se", 83 + "discord": "Discord", 84 + "discordId": "Discord användar-ID", 85 + "discordIdPlaceholder": "Ditt Discord användar-ID", 86 + "discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)", 87 + "telegram": "Telegram", 88 + "telegramUsername": "Telegram-användarnamn", 89 + "telegramUsernamePlaceholder": "@dittanvändarnamn", 90 + "signal": "Signal", 91 + "signalNumber": "Signal-telefonnummer", 92 + "signalNumberPlaceholder": "+46701234567", 93 + "signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)", 94 + "inviteCode": "Inbjudningskod", 95 + "inviteCodePlaceholder": "Ange din inbjudningskod", 96 + "inviteCodeRequired": "krävs", 97 + "createButton": "Skapa konto", 98 + "creating": "Skapar konto...", 99 + "alreadyHaveAccount": "Har du redan ett konto?", 100 + "signIn": "Logga in", 101 + "wantPasswordless": "Vill du ha lösenordsfri säkerhet?", 102 + "createPasskeyAccount": "Skapa ett nyckelbaserat konto", 103 + "validation": { 104 + "handleRequired": "Användarnamn krävs", 105 + "handleNoDots": "Användarnamn kan inte innehålla punkter. Du kan konfigurera ett eget domännamn efter att kontot skapats.", 106 + "passwordRequired": "Lösenord krävs", 107 + "passwordLength": "Lösenordet måste vara minst 8 tecken", 108 + "passwordsMismatch": "Lösenorden matchar inte", 109 + "inviteCodeRequired": "Inbjudningskod krävs", 110 + "externalDidRequired": "Extern did:web krävs", 111 + "externalDidFormat": "Extern DID måste börja med did:web:", 112 + "emailRequired": "E-post krävs för e-postverifiering", 113 + "discordIdRequired": "Discord-ID krävs för Discord-verifiering", 114 + "telegramRequired": "Telegram-användarnamn krävs för Telegram-verifiering", 115 + "signalRequired": "Telefonnummer krävs för Signal-verifiering" 116 + } 117 + }, 118 + "dashboard": { 119 + "title": "Kontrollpanel", 120 + "switchAccount": "Byt konto", 121 + "addAnotherAccount": "Lägg till ett annat konto", 122 + "signOut": "Logga ut @{handle}", 123 + "deactivatedTitle": "Konto inaktiverat", 124 + "deactivatedMessage": "Ditt konto är för närvarande inaktiverat. Detta sker vanligtvis under kontoflyttning. Vissa funktioner kan vara begränsade tills ditt konto återaktiveras.", 125 + "accountOverview": "Kontoöversikt", 126 + "handle": "Användarnamn", 127 + "did": "DID", 128 + "primaryContact": "Primär kontakt", 129 + "admin": "Administratör", 130 + "deactivated": "Inaktiverat", 131 + "verified": "Verifierad", 132 + "unverified": "Ej verifierad", 133 + "navAppPasswords": "Applösenord", 134 + "navAppPasswordsDesc": "Hantera lösenord för tredjepartsappar", 135 + "navSessions": "Aktiva sessioner", 136 + "navSessionsDesc": "Visa och hantera dina inloggningssessioner", 137 + "navInviteCodes": "Inbjudningskoder", 138 + "navInviteCodesDesc": "Visa och skapa inbjudningskoder", 139 + "navSettings": "Kontoinställningar", 140 + "navSettingsDesc": "E-post, lösenord, användarnamn och mer", 141 + "navSecurity": "Säkerhet", 142 + "navSecurityDesc": "Tvåfaktorsautentisering", 143 + "navComms": "Kommunikationsinställningar", 144 + "navCommsDesc": "Discord, Telegram, Signal-kanaler", 145 + "navRepo": "Dataförvarsutforskare", 146 + "navRepoDesc": "Bläddra och hantera råa AT Protocol-poster", 147 + "navAdmin": "Adminpanel", 148 + "navAdminDesc": "Serverstatistik och administratörsoperationer" 149 + }, 150 + "settings": { 151 + "title": "Kontoinställningar", 152 + "language": "Språk", 153 + "languageDescription": "Välj ditt föredragna språk", 154 + "changeEmail": "Ändra e-post", 155 + "currentEmail": "Nuvarande: {email}", 156 + "newEmail": "Ny e-post", 157 + "newEmailPlaceholder": "ny@exempel.se", 158 + "changeEmailButton": "Ändra e-post", 159 + "requesting": "Begär...", 160 + "verificationCode": "Verifieringskod", 161 + "verificationCodePlaceholder": "Ange kod från e-post", 162 + "confirmEmailChange": "Bekräfta e-poständring", 163 + "updating": "Uppdaterar...", 164 + "changeHandle": "Ändra användarnamn", 165 + "currentHandle": "Nuvarande: @{handle}", 166 + "pdsHandle": "PDS-användarnamn", 167 + "customDomain": "Egen domän", 168 + "customDomainDescription": "Använd din egen domän som användarnamn. Du måste verifiera domänägande först.", 169 + "setupInstructions": "Installationsanvisningar", 170 + "setupMethodsIntro": "Välj en av dessa verifieringsmetoder:", 171 + "dnsMethod": "Alternativ 1: DNS TXT-post (Rekommenderas)", 172 + "dnsMethodDesc": "Lägg till denna TXT-post till din domän:", 173 + "httpMethod": "Alternativ 2: HTTP Well-Known-fil", 174 + "httpMethodDesc": "Tillhandahåll din DID på denna URL:", 175 + "httpMethodContent": "Filen ska endast innehålla:", 176 + "yourDomain": "Din domän", 177 + "yourDomainPlaceholder": "exempel.se", 178 + "verifyAndUpdate": "Verifiera och uppdatera användarnamn", 179 + "verifying": "Verifierar...", 180 + "newHandle": "Nytt användarnamn", 181 + "newHandlePlaceholder": "dittanvändarnamn", 182 + "changeHandleButton": "Ändra användarnamn", 183 + "changePassword": "Ändra lösenord", 184 + "currentPassword": "Nuvarande lösenord", 185 + "currentPasswordPlaceholder": "Ange nuvarande lösenord", 186 + "newPassword": "Nytt lösenord", 187 + "newPasswordPlaceholder": "Minst 8 tecken", 188 + "confirmNewPassword": "Bekräfta nytt lösenord", 189 + "confirmNewPasswordPlaceholder": "Bekräfta nytt lösenord", 190 + "changePasswordButton": "Ändra lösenord", 191 + "changing": "Ändrar...", 192 + "exportData": "Exportera data", 193 + "exportDataDescription": "Ladda ner hela ditt arkiv som en CAR-fil (Content Addressable Archive). Detta inkluderar alla dina inlägg, gillanden, följningar och annan data.", 194 + "downloadRepo": "Ladda ner arkiv", 195 + "exporting": "Exporterar...", 196 + "deleteAccount": "Radera konto", 197 + "deleteWarning": "Denna åtgärd är oåterkallelig. All din data kommer att raderas permanent.", 198 + "requestDeletion": "Begär kontoradering", 199 + "confirmationCode": "Bekräftelsekod (från e-post)", 200 + "confirmationCodePlaceholder": "Ange bekräftelsekod", 201 + "yourPassword": "Ditt lösenord", 202 + "yourPasswordPlaceholder": "Ange ditt lösenord", 203 + "permanentlyDelete": "Radera konto permanent", 204 + "deleting": "Raderar...", 205 + "messages": { 206 + "emailCodeSent": "Verifieringskod skickad till din nuvarande e-post", 207 + "emailUpdated": "E-post uppdaterad", 208 + "handleUpdated": "Användarnamn uppdaterat", 209 + "passwordChanged": "Lösenord ändrat", 210 + "passwordsMismatch": "Lösenorden matchar inte", 211 + "passwordLength": "Lösenordet måste vara minst 8 tecken", 212 + "deletionCodeSent": "Bekräftelse för radering skickad till din e-post", 213 + "repoExported": "Arkiv exporterat", 214 + "confirmDelete": "Är du helt säker på att du vill radera ditt konto? Detta kan inte ångras." 215 + } 216 + }, 217 + "appPasswords": { 218 + "title": "Applösenord", 219 + "description": "Applösenord låter dig logga in i tredjepartsappar utan att ge dem ditt huvudlösenord. Varje applösenord kan återkallas individuellt.", 220 + "createNew": "Skapa nytt applösenord", 221 + "appNamePlaceholder": "Appnamn (t.ex. Graysky, Skeets)", 222 + "created": "Applösenord skapat", 223 + "createdMessage": "Kopiera detta lösenord nu. Du kommer inte att kunna se det igen.", 224 + "yourPasswords": "Dina applösenord", 225 + "noPasswords": "Inga applösenord ännu", 226 + "revoke": "Återkalla", 227 + "revoking": "Återkallar...", 228 + "creating": "Skapar...", 229 + "revokeConfirm": "Återkalla applösenord \"{name}\"? Appar som använder detta lösenord kommer inte längre att kunna komma åt ditt konto." 230 + }, 231 + "sessions": { 232 + "title": "Aktiva sessioner", 233 + "loadingSessions": "Laddar sessioner...", 234 + "noSessions": "Inga aktiva sessioner hittades.", 235 + "current": "Nuvarande", 236 + "oauth": "OAuth", 237 + "session": "Session", 238 + "signOut": "Logga ut", 239 + "revoke": "Återkalla", 240 + "revokeAll": "Återkalla alla andra sessioner", 241 + "revokeCurrentConfirm": "Detta loggar ut dig från denna session. Fortsätt?", 242 + "revokeConfirm": "Återkalla denna session?", 243 + "revokeAllConfirm": "Detta kommer att återkalla {count} andra sessioner. Fortsätt?", 244 + "noOtherSessions": "Inga andra sessioner att återkalla", 245 + "failedToLoad": "Kunde inte ladda sessioner", 246 + "failedToRevoke": "Kunde inte återkalla session", 247 + "failedToRevokeAll": "Kunde inte återkalla sessioner", 248 + "created": "Skapad:", 249 + "expires": "Upphör:", 250 + "daysAgo": "{count} dagar sedan", 251 + "hoursAgo": "{count} timmar sedan", 252 + "minutesAgo": "{count} minuter sedan", 253 + "justNow": "Just nu" 254 + }, 255 + "inviteCodes": { 256 + "title": "Inbjudningskoder", 257 + "description": "Inbjudningskoder låter dig bjuda in vänner. Varje kod kan användas en gång.", 258 + "createNew": "Skapa ny inbjudningskod", 259 + "uses": "Användningar", 260 + "usesPlaceholder": "Antal användningar (1-100)", 261 + "yourCodes": "Dina inbjudningskoder", 262 + "noCodes": "Inga inbjudningskoder ännu", 263 + "available": "Tillgänglig", 264 + "used": "Använd av @{handle}", 265 + "disabled": "Inaktiverad", 266 + "usedBy": "Använd av", 267 + "creating": "Skapar...", 268 + "disableConfirm": "Inaktivera denna inbjudningskod? Den kan inte längre användas.", 269 + "created": "Inbjudningskod skapad", 270 + "copy": "Kopiera", 271 + "createdOn": "Skapad {date}" 272 + }, 273 + "security": { 274 + "title": "Säkerhet", 275 + "passkeys": "Nycklar", 276 + "passkeysDescription": "Nycklar ger säker, lösenordsfri autentisering med din enhets inbyggda säkerhet (fingeravtryck, ansikte eller PIN).", 277 + "addPasskey": "Lägg till nyckel", 278 + "adding": "Lägger till...", 279 + "noPasskeys": "Inga nycklar registrerade", 280 + "passkeyName": "Nyckelnamn", 281 + "passkeyNamePlaceholder": "t.ex. MacBook Pro, iPhone", 282 + "register": "Registrera", 283 + "registering": "Registrerar...", 284 + "rename": "Byt namn", 285 + "renaming": "Byter namn...", 286 + "deletePasskey": "Radera", 287 + "deletePasskeyConfirm": "Radera nyckel \"{name}\"? Du kommer inte att kunna använda den för att logga in längre.", 288 + "totp": "Autentiseringsapp (TOTP)", 289 + "totpDescription": "Använd en autentiseringsapp som Google Authenticator, Authy eller 1Password för tvåfaktorsautentisering.", 290 + "totpEnabled": "TOTP är aktiverat", 291 + "totpDisabled": "TOTP är inte aktiverat", 292 + "enableTotp": "Aktivera TOTP", 293 + "disableTotp": "Inaktivera TOTP", 294 + "disabling": "Inaktiverar...", 295 + "totpSetup": "Konfigurera autentiseringsapp", 296 + "totpSetupInstructions": "Skanna denna QR-kod med din autentiseringsapp och ange sedan den 6-siffriga koden för att verifiera.", 297 + "totpCode": "Verifieringskod", 298 + "totpCodePlaceholder": "Ange 6-siffrig kod", 299 + "verifyAndEnable": "Verifiera och aktivera", 300 + "backupCodes": "Reservkoder", 301 + "backupCodesDescription": "Använd dessa koder för att logga in om du förlorar tillgång till din autentiseringsapp. Varje kod kan endast användas en gång.", 302 + "regenerateBackupCodes": "Generera nya reservkoder", 303 + "regenerating": "Genererar...", 304 + "regenerateConfirm": "Generera nya reservkoder? Dina nuvarande koder kommer inte längre att fungera.", 305 + "legacyLogin": "Föråldrad inloggning", 306 + "legacyLoginDescription": "Tillåt inloggning med användarnamn/lösenord direkt (föråldrat läge). När detta är inaktiverat måste du använda OAuth med MFA.", 307 + "legacyLoginOn": "Föråldrad inloggning är aktiverad", 308 + "legacyLoginOff": "Föråldrad inloggning är inaktiverad", 309 + "enableLegacyLogin": "Aktivera föråldrad inloggning", 310 + "disableLegacyLogin": "Inaktivera föråldrad inloggning", 311 + "legacyLoginWarning": "Varning: Att aktivera föråldrad inloggning kringgår MFA för direkta lösenordsinloggningar. Aktivera endast om det behövs för appkompatibilitet.", 312 + "totpPasswordWarning": "Med TOTP aktiverat blockeras lösenordsändringar från Bluesky-appen (eller andra föråldrade appar). För att ändra ditt lösenord har du två alternativ:", 313 + "totpPasswordOption1Label": "Ändra det här:", 314 + "totpPasswordOption1Text": "Använd denna webbplats", 315 + "totpPasswordOption1Link": "Inställningssida", 316 + "totpPasswordOption1Suffix": "där du kan verifiera med din autentiseringsapp.", 317 + "totpPasswordOption2Label": "Verifiera din session först:", 318 + "totpPasswordOption2Text": "Använd", 319 + "totpPasswordOption2Link": "återautentiseringsalternativet", 320 + "totpPasswordOption2Suffix": "för att verifiera din Bluesky-session med TOTP, sedan fungerar lösenordsändringar tillfälligt.", 321 + "legacyAppsTitle": "Vad är föråldrade appar?", 322 + "legacyAppsDescription": "Vissa appar (som den officiella Bluesky-appen) använder föråldrad autentisering som endast kräver ditt lösenord. När du har MFA aktiverat kringgår dessa appar din andra faktor. Att inaktivera föråldrad inloggning tvingar alla appar att använda OAuth, som korrekt tillämpar MFA.", 323 + "password": "Lösenord", 324 + "passwordStatus": "Du har ett lösenord inställt", 325 + "noPassword": "Inget lösenord inställt (endast nyckelkonto)", 326 + "setPassword": "Ställ in lösenord", 327 + "removePassword": "Ta bort lösenord", 328 + "removePasswordConfirm": "Ta bort ditt lösenord? Du måste använda nycklar för att logga in.", 329 + "removing": "Tar bort...", 330 + "loading": "Laddar...", 331 + "loadingPasskeys": "Laddar nycklar...", 332 + "cancel": "Avbryt", 333 + "save": "Spara", 334 + "back": "Tillbaka", 335 + "next": "Nästa: Verifiera kod", 336 + "copyToClipboard": "Kopiera till urklipp", 337 + "savedMyCodes": "Jag har sparat mina koder", 338 + "cantScan": "Kan du inte skanna? Ange manuellt", 339 + "unnamedPasskey": "Namnlös nyckel", 340 + "added": "Tillagd", 341 + "lastUsed": "Senast använd", 342 + "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.", 343 + "disableTotpWarning": "Detta gör ditt konto mindre säkert.", 344 + "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.", 345 + "beforeProceeding": "Innan du fortsätter:", 346 + "beforeProceedingItem1": "Se till att du har minst en pålitlig nyckel registrerad", 347 + "beforeProceedingItem2": "Överväg att registrera nycklar på flera enheter", 348 + "beforeProceedingItem3": "Se till att din återställningsnotifieringskanal är uppdaterad", 349 + "addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.", 350 + "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.", 351 + "trustedDevices": "Betrodda enheter", 352 + "trustedDevicesDescription": "Hantera enheter som kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.", 353 + "manageTrustedDevices": "Hantera betrodda enheter", 354 + "appCompatibility": "Appkompatibilitet", 355 + "enterPassword": "Ange ditt lösenord", 356 + "legacyLoginEnabled": "Föråldrad appinloggning aktiverad", 357 + "legacyLoginDisabled": "Föråldrad appinloggning inaktiverad - endast OAuth-appar kan logga in", 358 + "failedToUpdatePreference": "Kunde inte uppdatera inställning", 359 + "passwordRemoved": "Lösenord borttaget. Ditt konto är nu endast nyckelkonto.", 360 + "failedToRemovePassword": "Kunde inte ta bort lösenord", 361 + "failedToLoadTotpStatus": "Kunde inte ladda TOTP-status", 362 + "totpEnabledSuccess": "Tvåfaktorsautentisering aktiverad", 363 + "totpDisabledSuccess": "Tvåfaktorsautentisering inaktiverad", 364 + "backupCodesCopied": "Reservkoder kopierade till urklipp", 365 + "failedToLoadPasskeys": "Kunde inte ladda nycklar", 366 + "passkeysNotSupported": "Nycklar stöds inte i denna webbläsare", 367 + "passkeyCreationCancelled": "Nyckelskapande avbröts", 368 + "passkeyAddedSuccess": "Nyckel tillagd", 369 + "passkeyDeleted": "Nyckel raderad", 370 + "passkeyRenamed": "Nyckel omdöpt" 371 + }, 372 + "comms": { 373 + "title": "Kommunikationsinställningar", 374 + "description": "Välj hur du vill ta emot viktiga meddelanden som lösenordsåterställningar, säkerhetsvarningar och kontouppdateringar.", 375 + "preferredChannel": "Föredragen kanal", 376 + "preferredChannelDescription": "Välj ditt föredragna sätt att ta emot meddelanden. Du måste konfigurera en kanal innan du kan välja den.", 377 + "channelConfiguration": "Kanalkonfiguration", 378 + "emailVia": "Ta emot meddelanden via e-post", 379 + "discordVia": "Ta emot meddelanden via Discord DM", 380 + "telegramVia": "Ta emot meddelanden via Telegram", 381 + "signalVia": "Ta emot meddelanden via Signal", 382 + "configureToEnable": "Konfigurera nedan för att aktivera", 383 + "emailManagedInSettings": "Din e-post hanteras i Kontoinställningar", 384 + "discordIdHint": "Ditt Discord användar-ID (inte användarnamn). Aktivera Utvecklarläge i Discord för att kopiera det.", 385 + "telegramHint": "Ditt Telegram-användarnamn utan @-symbolen", 386 + "signalHint": "Ditt Signal-telefonnummer med landskod", 387 + "primary": "Primär", 388 + "verified": "Verifierad", 389 + "notVerified": "Ej verifierad", 390 + "verifyButton": "Verifiera", 391 + "verifyCodePlaceholder": "Ange verifieringskod", 392 + "submit": "Skicka", 393 + "saving": "Sparar...", 394 + "savePreferences": "Spara inställningar", 395 + "preferencesSaved": "Kommunikationsinställningar sparade", 396 + "verifiedSuccess": "{channel} verifierad", 397 + "messageHistory": "Meddelandehistorik", 398 + "historyDescription": "Visa senaste meddelanden skickade till ditt konto.", 399 + "loadHistory": "Ladda historik", 400 + "hideHistory": "Dölj historik", 401 + "noMessages": "Inga meddelanden hittades.", 402 + "sent": "skickad", 403 + "failed": "misslyckades" 404 + }, 405 + "repoExplorer": { 406 + "title": "Dataförvarsutforskare", 407 + "description": "Bläddra och hantera dina råa AT Protocol-poster.", 408 + "collections": "Samlingar", 409 + "noCollections": "Inga samlingar hittades", 410 + "records": "Poster", 411 + "noRecords": "Inga poster i denna samling", 412 + "recordDetails": "Postdetaljer", 413 + "rkey": "Postnyckel", 414 + "cid": "CID", 415 + "value": "Värde", 416 + "deleteRecord": "Radera post", 417 + "deleteConfirm": "Radera post {rkey}? Detta kan inte ångras.", 418 + "unknownError": "Ett okänt fel uppstod", 419 + "invalidJson": "Ogiltig JSON", 420 + "collectionRequired": "Samling krävs", 421 + "recordCreated": "Post skapad: {uri}", 422 + "recordUpdated": "Post uppdaterad", 423 + "recordDeleted": "Post raderad", 424 + "newRecord": "Ny post", 425 + "createRecord": "Skapa post", 426 + "filterCollections": "Filtrera samlingar...", 427 + "filterRecords": "Filtrera poster...", 428 + "noCollectionsYet": "Inga samlingar ännu. Skapa din första post för att komma igång.", 429 + "loadMore": "Ladda fler", 430 + "recordJson": "Post-JSON", 431 + "saving": "Sparar...", 432 + "updateRecord": "Uppdatera post", 433 + "collectionNsid": "Samling (NSID)", 434 + "recordKeyOptional": "Postnyckel (valfri)", 435 + "autoGenerated": "Genereras automatiskt om tom (TID)", 436 + "autoGeneratedHint": "Lämna tom för att automatiskt generera en TID-baserad nyckel", 437 + "creating": "Skapar...", 438 + "demoPostText": "Hej från min PDS! Detta är mitt första inlägg.", 439 + "demoDisplayName": "Ditt visningsnamn", 440 + "demoBio": "En kort presentation om dig själv." 441 + }, 442 + "admin": { 443 + "title": "Adminpanel", 444 + "serverStats": "Serverstatistik", 445 + "users": "Användare", 446 + "repos": "Dataförvar", 447 + "records": "Poster", 448 + "blobStorage": "Bloblagring", 449 + "refreshStats": "Uppdatera statistik", 450 + "userManagement": "Användarhantering", 451 + "searchPlaceholder": "Sök på användarnamn (valfritt)", 452 + "searchUsers": "Sök användare", 453 + "noUsers": "Inga användare hittades", 454 + "handle": "Användarnamn", 455 + "email": "E-post", 456 + "status": "Status", 457 + "created": "Skapad", 458 + "loadMore": "Ladda fler", 459 + "inviteCodes": "Inbjudningskoder", 460 + "loadInviteCodes": "Ladda inbjudningskoder", 461 + "refresh": "Uppdatera", 462 + "noInvites": "Inga inbjudningskoder hittades", 463 + "code": "Kod", 464 + "available": "Tillgänglig", 465 + "uses": "Användningar", 466 + "actions": "Åtgärder", 467 + "disable": "Inaktivera", 468 + "disableInviteConfirm": "Inaktivera inbjudningskod {code}?", 469 + "active": "Aktiv", 470 + "exhausted": "Förbrukad", 471 + "disabled": "Inaktiverad", 472 + "userDetails": "Användardetaljer", 473 + "did": "DID", 474 + "invites": "Inbjudningar", 475 + "enabled": "Aktiverad", 476 + "enableInvites": "Aktivera inbjudningar", 477 + "disableInvites": "Inaktivera inbjudningar", 478 + "deleteAccount": "Radera konto", 479 + "deleteConfirm": "Radera konto @{handle}? Detta kan inte ångras.", 480 + "verified": "Verifierad", 481 + "unverified": "Ej verifierad", 482 + "deactivated": "Inaktiverad" 483 + }, 484 + "oauth": { 485 + "login": { 486 + "title": "Logga in", 487 + "subtitle": "Logga in för att fortsätta till applikationen", 488 + "signingIn": "Loggar in...", 489 + "authenticating": "Autentiserar...", 490 + "checkingPasskey": "Kontrollerar nyckel...", 491 + "signInWithPasskey": "Logga in med nyckel", 492 + "passkeyNotSetUp": "Nyckel inte konfigurerad", 493 + "orUsePassword": "eller använd lösenord", 494 + "password": "Lösenord", 495 + "rememberDevice": "Kom ihåg denna enhet", 496 + "passkeyHintChecking": "Kontrollerar nyckelstatus...", 497 + "passkeyHintAvailable": "Logga in med din nyckel", 498 + "passkeyHintNotAvailable": "Inga nycklar registrerade för detta konto" 499 + }, 500 + "consent": { 501 + "title": "Auktorisera applikation", 502 + "appWantsAccess": "{app} vill ha tillgång till ditt konto", 503 + "permissions": "Denna applikation kommer att kunna:", 504 + "readProfile": "Läsa din profilinformation", 505 + "readPosts": "Läsa dina inlägg och innehåll", 506 + "writePosts": "Skapa och radera inlägg för din räkning", 507 + "readNotifications": "Läsa dina notiser", 508 + "fullAccess": "Full tillgång till ditt konto", 509 + "authorize": "Auktorisera", 510 + "deny": "Neka", 511 + "authorizing": "Auktoriserar...", 512 + "rememberChoice": "Kom ihåg detta val", 513 + "signingInAs": "Loggar in som:", 514 + "permissionsRequested": "Begärda behörigheter", 515 + "required": "Krävs", 516 + "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation" 517 + }, 518 + "accounts": { 519 + "title": "Välj konto", 520 + "subtitle": "Välj ett konto för att fortsätta", 521 + "useAnother": "Använd ett annat konto" 522 + }, 523 + "twoFactor": { 524 + "title": "Tvåfaktorsautentisering", 525 + "subtitle": "Ytterligare verifiering krävs", 526 + "usePasskey": "Använd nyckel", 527 + "useTotp": "Använd autentiseringsapp", 528 + "verifying": "Verifierar..." 529 + }, 530 + "twoFactorCode": { 531 + "title": "Tvåfaktorsautentisering", 532 + "subtitle": "En verifieringskod har skickats till din {channel}. Ange koden nedan för att fortsätta.", 533 + "codeLabel": "Verifieringskod", 534 + "codePlaceholder": "Ange 6-siffrig kod", 535 + "verify": "Verifiera", 536 + "verifying": "Verifierar...", 537 + "errors": { 538 + "missingRequestUri": "Saknar request_uri-parameter", 539 + "verificationFailed": "Verifiering misslyckades", 540 + "connectionFailed": "Kunde inte ansluta till servern", 541 + "unexpectedResponse": "Oväntat svar från servern" 542 + } 543 + }, 544 + "totp": { 545 + "title": "Ange autentiseringskod", 546 + "subtitle": "Ange den 6-siffriga koden från din autentiseringsapp", 547 + "codePlaceholder": "Ange 6-siffrig kod", 548 + "verify": "Verifiera", 549 + "verifying": "Verifierar...", 550 + "useBackupCode": "Använd reservkod istället", 551 + "backupCodePlaceholder": "Ange reservkod", 552 + "trustDevice": "Lita på denna enhet i 30 dagar", 553 + "hintBackupCode": "Använder reservkod", 554 + "hintTotpCode": "Använder autentiseringskod", 555 + "hintDefault": "6 siffror för autentiserare, 8 tecken för reservkod" 556 + }, 557 + "passkey": { 558 + "title": "Nyckelverifiering", 559 + "subtitle": "Använd din nyckel för att verifiera din identitet", 560 + "waiting": "Väntar på nyckel...", 561 + "useTotp": "Använd autentiseringsapp istället" 562 + }, 563 + "error": { 564 + "title": "Auktoriseringsfel", 565 + "genericError": "Ett fel uppstod under auktorisering.", 566 + "tryAgain": "Försök igen", 567 + "backToApp": "Tillbaka till applikationen" 568 + } 569 + }, 570 + "verify": { 571 + "title": "Verifiera ditt konto", 572 + "subtitle": "Vi har skickat en verifieringskod till din {channel}. Ange den nedan för att slutföra registreringen.", 573 + "codePlaceholder": "Ange 6-siffrig kod", 574 + "codeLabel": "Verifieringskod", 575 + "verifyButton": "Verifiera konto", 576 + "verifying": "Verifierar...", 577 + "resendCode": "Skicka kod igen", 578 + "resending": "Skickar igen...", 579 + "codeResent": "Verifieringskod skickad igen!", 580 + "backToLogin": "Tillbaka till inloggning", 581 + "verifyingAccount": "Verifierar konto: @{handle}", 582 + "startOver": "Börja om med ett annat konto", 583 + "noPending": "Ingen väntande verifiering hittades.", 584 + "noPendingInfo": "Om du nyligen skapade ett konto och behöver verifiera det kan du behöva skapa ett nytt konto. Om du redan verifierat ditt konto kan du logga in.", 585 + "createAccount": "Skapa konto", 586 + "signIn": "Logga in" 587 + }, 588 + "resetPassword": { 589 + "title": "Återställ lösenord", 590 + "forgotTitle": "Glömt lösenord", 591 + "subtitle": "Ange koden du fick och välj ett nytt lösenord.", 592 + "forgotSubtitle": "Ange ditt användarnamn eller e-post så skickar vi dig en kod för att återställa ditt lösenord.", 593 + "handleOrEmail": "Användarnamn eller e-post", 594 + "emailPlaceholder": "användarnamn eller du@exempel.se", 595 + "sendCode": "Skicka återställningskod", 596 + "sending": "Skickar...", 597 + "codeSent": "Återställningskod skickad! Kontrollera din föredragna notifieringskanal.", 598 + "enterCode": "Ange koden från din e-post och ditt nya lösenord.", 599 + "code": "Återställningskod", 600 + "codePlaceholder": "Ange återställningskod", 601 + "newPassword": "Nytt lösenord", 602 + "newPasswordPlaceholder": "Minst 8 tecken", 603 + "confirmPassword": "Bekräfta lösenord", 604 + "confirmPasswordPlaceholder": "Bekräfta nytt lösenord", 605 + "resetButton": "Återställ lösenord", 606 + "resetting": "Återställer...", 607 + "success": "Lösenord återställt!", 608 + "backToLogin": "Tillbaka till inloggning", 609 + "requestNewCode": "Begär ny kod", 610 + "passwordsMismatch": "Lösenorden matchar inte", 611 + "passwordLength": "Lösenordet måste vara minst 8 tecken" 612 + }, 613 + "recoverPasskey": { 614 + "title": "Återställ ditt konto", 615 + "invalidLinkTitle": "Ogiltig återställningslänk", 616 + "invalidLinkMessage": "Denna återställningslänk är ogiltig eller har skadats. Begär ett nytt återställningsmeddelande.", 617 + "goToLogin": "Gå till inloggning", 618 + "successTitle": "Lösenord inställt!", 619 + "successMessage": "Ditt tillfälliga lösenord har ställts in. Du kan nu logga in med detta lösenord.", 620 + "successNextSteps": "Efter inloggning rekommenderar vi att du lägger till en ny nyckel i dina säkerhetsinställningar för att återställa endast nyckelautentisering.", 621 + "signIn": "Logga in", 622 + "subtitle": "Ställ in ett tillfälligt lösenord för att återfå tillgång till ditt endast nyckelkonto.", 623 + "newPassword": "Nytt lösenord", 624 + "newPasswordPlaceholder": "Minst 8 tecken", 625 + "confirmPassword": "Bekräfta lösenord", 626 + "confirmPasswordPlaceholder": "Bekräfta ditt lösenord", 627 + "whatHappensNext": "Vad händer härnäst?", 628 + "whatHappensNextDetail": "Efter att du ställt in detta lösenord kan du logga in och lägga till en ny nyckel i dina säkerhetsinställningar. När du har en ny nyckel kan du valfritt ta bort det tillfälliga lösenordet.", 629 + "setPassword": "Ställ in lösenord", 630 + "settingPassword": "Ställer in lösenord...", 631 + "validation": { 632 + "passwordRequired": "Nytt lösenord krävs", 633 + "passwordLength": "Lösenordet måste vara minst 8 tecken", 634 + "passwordsMismatch": "Lösenorden matchar inte" 635 + }, 636 + "errors": { 637 + "invalidLink": "Ogiltig återställningslänk. Begär en ny.", 638 + "expired": "Denna återställningslänk har gått ut. Begär en ny." 639 + } 640 + }, 641 + "requestPasskeyRecovery": { 642 + "title": "Återställ nyckelkonto", 643 + "subtitle": "Förlorat tillgång till din nyckel? Ange ditt användarnamn eller e-post så skickar vi dig en återställningslänk.", 644 + "successTitle": "Återställningslänk skickad", 645 + "successMessage": "Om ditt konto finns och är ett endast nyckelkonto får du en återställningslänk på din föredragna notifieringskanal.", 646 + "successInfo": "Länken upphör om 1 timme. Kontrollera din e-post, Discord, Telegram eller Signal beroende på dina kontoinställningar.", 647 + "handleOrEmail": "Användarnamn eller e-post", 648 + "emailPlaceholder": "användarnamn eller du@exempel.se", 649 + "howItWorks": "Så fungerar det", 650 + "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.", 651 + "sendRecoveryLink": "Skicka återställningslänk", 652 + "sending": "Skickar...", 653 + "backToLogin": "Tillbaka till inloggning" 654 + }, 655 + "registerPasskey": { 656 + "title": "Skapa nyckelkonto", 657 + "subtitle": "Skapa ett lösenordsfritt konto med en nyckel.", 658 + "handle": "Användarnamn", 659 + "handlePlaceholder": "dittnamn", 660 + "handleHint": "Ditt fullständiga användarnamn blir: @{handle}", 661 + "email": "E-postadress", 662 + "emailPlaceholder": "du@exempel.se", 663 + "inviteCode": "Inbjudningskod", 664 + "inviteCodePlaceholder": "Ange din inbjudningskod", 665 + "createButton": "Skapa konto", 666 + "creating": "Skapar...", 667 + "alreadyHaveAccount": "Har du redan ett konto?", 668 + "signIn": "Logga in", 669 + "wantPassword": "Vill du använda ett lösenord?", 670 + "createPasswordAccount": "Skapa ett lösenordskonto" 671 + }, 672 + "trustedDevices": { 673 + "title": "Betrodda enheter", 674 + "backToSecurity": "← Säkerhetsinställningar", 675 + "description": "Betrodda enheter kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.", 676 + "noDevices": "Inga betrodda enheter ännu.", 677 + "noDevicesHint": "När du loggar in med tvåfaktorsautentisering aktiverat kan du välja att lita på enheten i 30 dagar.", 678 + "lastSeen": "Senast sedd:", 679 + "trustedSince": "Betrodd sedan:", 680 + "trustExpires": "Förtroende upphör:", 681 + "expired": "Upphört", 682 + "tomorrow": "I morgon", 683 + "inDays": "Om {days} dagar", 684 + "revoke": "Återkalla förtroende", 685 + "revokeConfirm": "Är du säker på att du vill återkalla förtroendet för denna enhet? Du måste ange din 2FA-kod nästa gång du loggar in från denna enhet.", 686 + "deviceRevoked": "Enhetsförtroende återkallat", 687 + "deviceRenamed": "Enhet omdöpt", 688 + "deviceNamePlaceholder": "Enhetsnamn", 689 + "browser": "Webbläsare:", 690 + "unknownDevice": "Okänd enhet" 691 + }, 692 + "reauth": { 693 + "title": "Återautentisering krävs", 694 + "subtitle": "Verifiera din identitet för att fortsätta.", 695 + "usePassword": "Använd lösenord", 696 + "usePasskey": "Använd nyckel", 697 + "useTotp": "Använd autentiserare", 698 + "passwordPlaceholder": "Ange ditt lösenord", 699 + "totpPlaceholder": "Ange 6-siffrig kod", 700 + "verify": "Verifiera", 701 + "verifying": "Verifierar...", 702 + "cancel": "Avbryt" 703 + } 704 + }
+2
frontend/src/locales/zh.json
··· 306 306 "legacyLoginDescription": "允许使用用户名/密码直接登录(传统模式)。禁用后必须使用 OAuth + 双重验证。", 307 307 "legacyLoginOn": "传统登录已启用", 308 308 "legacyLoginOff": "传统登录已禁用", 309 + "enableLegacyLogin": "启用传统登录", 310 + "disableLegacyLogin": "禁用传统登录", 309 311 "legacyLoginWarning": "警告:启用传统登录会绕过双重身份验证。仅在需要兼容旧版应用时启用。", 310 312 "totpPasswordWarning": "启用 TOTP 后,将无法从 Bluesky 应用(或其他旧版应用)更改密码。要更改密码,您有两个选择:", 311 313 "totpPasswordOption1Label": "在这里更改:",
+2 -2
frontend/src/routes/Admin.svelte
··· 350 350 {/if} 351 351 </div> 352 352 {#if selectedUser} 353 - <div class="modal-overlay" onclick={closeUserDetail} role="presentation"> 354 - <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true"> 353 + <div class="modal-overlay" onclick={closeUserDetail} onkeydown={(e) => e.key === 'Escape' && closeUserDetail()} role="presentation"> 354 + <div class="modal" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1"> 355 355 <div class="modal-header"> 356 356 <h2>User Details</h2> 357 357 <button class="close-btn" onclick={closeUserDetail}>&times;</button>
-6
frontend/src/routes/OAuthPasskey.svelte
··· 294 294 opacity: 0.6; 295 295 cursor: not-allowed; 296 296 } 297 - 298 - .help-text { 299 - font-size: 0.875rem; 300 - color: var(--text-muted); 301 - margin: 0; 302 - } 303 297 </style>
+8 -2
frontend/src/routes/Security.svelte
··· 1 1 <script lang="ts"> 2 - import { getAuthState } from '../lib/auth.svelte' 2 + import { getAuthState, getValidToken } from '../lib/auth.svelte' 3 3 import { navigate } from '../lib/router.svelte' 4 4 import { api, ApiError } from '../lib/api' 5 5 import ReauthModal from '../components/ReauthModal.svelte' ··· 128 128 if (!auth.session) return 129 129 removePasswordLoading = true 130 130 try { 131 - await api.removePassword(auth.session.accessJwt) 131 + const token = await getValidToken() 132 + if (!token) { 133 + showMessage('error', 'Session expired. Please log in again.') 134 + return 135 + } 136 + await api.removePassword(token) 132 137 hasPassword = false 133 138 showRemovePasswordForm = false 134 139 showMessage('success', $_('security.passwordRemoved')) ··· 747 752 class="toggle-button {allowLegacyLogin ? 'on' : 'off'}" 748 753 onclick={handleToggleLegacyLogin} 749 754 disabled={legacyLoginUpdating} 755 + aria-label={allowLegacyLogin ? $_('security.disableLegacyLogin') : $_('security.enableLegacyLogin')} 750 756 > 751 757 <span class="toggle-slider"></span> 752 758 </button>
+1 -1
src/api/server/password.rs
··· 471 471 .await; 472 472 } 473 473 474 - if crate::api::server::reauth::check_reauth_required(&state.db, &auth.0.did).await { 474 + if crate::api::server::reauth::check_reauth_required_cached(&state.db, &state.cache, &auth.0.did).await { 475 475 return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await; 476 476 } 477 477
+46 -4
src/api/server/reauth.rs
··· 128 128 } 129 129 } 130 130 131 - match update_last_reauth(&state.db, &auth.0.did).await { 131 + match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 132 132 Ok(reauthed_at) => { 133 133 info!(did = %auth.0.did, "Re-auth successful via password"); 134 134 Json(ReauthResponse { reauthed_at }).into_response() ··· 186 186 .into_response(); 187 187 } 188 188 189 - match update_last_reauth(&state.db, &auth.0.did).await { 189 + match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 190 190 Ok(reauthed_at) => { 191 191 info!(did = %auth.0.did, "Re-auth successful via TOTP"); 192 192 Json(ReauthResponse { reauthed_at }).into_response() ··· 394 394 395 395 let _ = crate::auth::webauthn::delete_authentication_state(&state.db, &auth.0.did).await; 396 396 397 - match update_last_reauth(&state.db, &auth.0.did).await { 397 + match update_last_reauth_cached(&state.db, &state.cache, &auth.0.did).await { 398 398 Ok(reauthed_at) => { 399 399 info!(did = %auth.0.did, "Re-auth successful via passkey"); 400 400 Json(ReauthResponse { reauthed_at }).into_response() ··· 410 410 } 411 411 } 412 412 413 - async fn update_last_reauth(db: &PgPool, did: &str) -> Result<DateTime<Utc>, sqlx::Error> { 413 + pub async fn update_last_reauth_cached( 414 + db: &PgPool, 415 + cache: &std::sync::Arc<dyn crate::cache::Cache>, 416 + did: &str, 417 + ) -> Result<DateTime<Utc>, sqlx::Error> { 414 418 let now = Utc::now(); 415 419 sqlx::query!( 416 420 "UPDATE session_tokens SET last_reauth_at = $1, mfa_verified = TRUE WHERE did = $2", ··· 419 423 ) 420 424 .execute(db) 421 425 .await?; 426 + let cache_key = format!("reauth:{}", did); 427 + let _ = cache 428 + .set( 429 + &cache_key, 430 + &now.timestamp().to_string(), 431 + std::time::Duration::from_secs(REAUTH_WINDOW_SECONDS as u64), 432 + ) 433 + .await; 422 434 Ok(now) 423 435 } 424 436 ··· 463 475 } 464 476 465 477 pub async fn check_reauth_required(db: &PgPool, did: &str) -> bool { 478 + let session = sqlx::query!( 479 + "SELECT last_reauth_at FROM session_tokens WHERE did = $1 ORDER BY created_at DESC LIMIT 1", 480 + did 481 + ) 482 + .fetch_optional(db) 483 + .await; 484 + 485 + match session { 486 + Ok(Some(row)) => is_reauth_required(row.last_reauth_at), 487 + _ => true, 488 + } 489 + } 490 + 491 + pub async fn check_reauth_required_cached( 492 + db: &PgPool, 493 + cache: &std::sync::Arc<dyn crate::cache::Cache>, 494 + did: &str, 495 + ) -> bool { 496 + let cache_key = format!("reauth:{}", did); 497 + if let Some(timestamp_str) = cache.get(&cache_key).await { 498 + if let Ok(timestamp) = timestamp_str.parse::<i64>() { 499 + let reauth_time = chrono::DateTime::from_timestamp(timestamp, 0); 500 + if let Some(t) = reauth_time { 501 + let elapsed = Utc::now().signed_duration_since(t); 502 + if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS { 503 + return false; 504 + } 505 + } 506 + } 507 + } 466 508 let session = sqlx::query!( 467 509 "SELECT last_reauth_at FROM session_tokens WHERE did = $1 ORDER BY created_at DESC LIMIT 1", 468 510 did
+113 -12
src/api/server/service_auth.rs
··· 8 8 }; 9 9 use serde::{Deserialize, Serialize}; 10 10 use serde_json::json; 11 - use tracing::error; 11 + use tracing::{error, info, warn}; 12 12 13 13 const HOUR_SECS: i64 = 3600; 14 14 const MINUTE_SECS: i64 = 60; ··· 49 49 headers: axum::http::HeaderMap, 50 50 Query(params): Query<GetServiceAuthParams>, 51 51 ) -> Response { 52 - let token = match crate::auth::extract_bearer_token_from_header( 53 - headers.get("Authorization").and_then(|h| h.to_str().ok()), 54 - ) { 55 - Some(t) => t, 56 - None => return ApiError::AuthenticationRequired.into_response(), 52 + let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 53 + let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok()); 54 + info!( 55 + has_auth_header = auth_header.is_some(), 56 + has_dpop_proof = dpop_proof.is_some(), 57 + aud = %params.aud, 58 + lxm = ?params.lxm, 59 + "getServiceAuth called" 60 + ); 61 + let auth_header = match auth_header { 62 + Some(h) => h.trim(), 63 + None => { 64 + warn!("getServiceAuth: no Authorization header"); 65 + return ApiError::AuthenticationRequired.into_response(); 66 + } 57 67 }; 58 - let auth_user = 68 + 69 + let (token, is_dpop) = if auth_header.len() >= 7 && auth_header[..7].eq_ignore_ascii_case("bearer ") { 70 + (auth_header[7..].trim().to_string(), false) 71 + } else if auth_header.len() >= 5 && auth_header[..5].eq_ignore_ascii_case("dpop ") { 72 + (auth_header[5..].trim().to_string(), true) 73 + } else { 74 + warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme"); 75 + return ApiError::AuthenticationRequired.into_response(); 76 + }; 77 + 78 + let auth_user = if is_dpop { 79 + match crate::oauth::verify::verify_oauth_access_token( 80 + &state.db, 81 + &token, 82 + dpop_proof, 83 + "GET", 84 + &format!("/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}", 85 + params.aud, 86 + params.lxm.as_deref().unwrap_or("")), 87 + ).await { 88 + Ok(result) => crate::auth::AuthenticatedUser { 89 + did: result.did, 90 + is_oauth: true, 91 + is_admin: false, 92 + scope: result.scope, 93 + key_bytes: None, 94 + }, 95 + Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 96 + return ( 97 + StatusCode::UNAUTHORIZED, 98 + [("DPoP-Nonce", nonce)], 99 + Json(json!({ 100 + "error": "use_dpop_nonce", 101 + "message": "DPoP nonce required" 102 + })), 103 + ).into_response(); 104 + } 105 + Err(e) => { 106 + warn!(error = ?e, "getServiceAuth DPoP auth validation failed"); 107 + return ( 108 + StatusCode::UNAUTHORIZED, 109 + Json(json!({ 110 + "error": "AuthenticationFailed", 111 + "message": format!("{:?}", e) 112 + })), 113 + ).into_response(); 114 + } 115 + } 116 + } else { 59 117 match crate::auth::validate_bearer_token_for_service_auth(&state.db, &token).await { 60 118 Ok(user) => user, 61 - Err(e) => return ApiError::from(e).into_response(), 62 - }; 119 + Err(e) => { 120 + warn!(error = ?e, "getServiceAuth auth validation failed"); 121 + return ApiError::from(e).into_response(); 122 + } 123 + } 124 + }; 125 + info!( 126 + did = %auth_user.did, 127 + is_oauth = auth_user.is_oauth, 128 + has_key = auth_user.key_bytes.is_some(), 129 + "getServiceAuth auth validated" 130 + ); 63 131 let key_bytes = match &auth_user.key_bytes { 64 132 Some(kb) => kb.clone(), 65 133 None => { 66 - return ApiError::AuthenticationFailedMsg( 67 - "OAuth tokens cannot create service auth".into(), 134 + warn!(did = %auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB"); 135 + match sqlx::query_as::<_, (Vec<u8>, Option<i32>)>( 136 + "SELECT k.key_bytes, k.encryption_version 137 + FROM users u 138 + JOIN user_keys k ON u.id = k.user_id 139 + WHERE u.did = $1" 68 140 ) 69 - .into_response(); 141 + .bind(&auth_user.did) 142 + .fetch_optional(&state.db) 143 + .await 144 + { 145 + Ok(Some((key_bytes_enc, encryption_version))) => { 146 + match crate::config::decrypt_key(&key_bytes_enc, encryption_version) { 147 + Ok(key) => key, 148 + Err(e) => { 149 + error!(error = ?e, "Failed to decrypt user key for service auth"); 150 + return ApiError::AuthenticationFailedMsg( 151 + "Failed to get signing key".into(), 152 + ) 153 + .into_response(); 154 + } 155 + } 156 + } 157 + Ok(None) => { 158 + return ApiError::AuthenticationFailedMsg( 159 + "User has no signing key".into(), 160 + ) 161 + .into_response(); 162 + } 163 + Err(e) => { 164 + error!(error = ?e, "DB error fetching user key"); 165 + return ApiError::AuthenticationFailedMsg( 166 + "Failed to get signing key".into(), 167 + ) 168 + .into_response(); 169 + } 170 + } 70 171 } 71 172 }; 72 173
+1 -1
src/api/server/session.rs
··· 1209 1209 } 1210 1210 } 1211 1211 1212 - const VALID_LOCALES: &[&str] = &["en", "zh", "ja", "ko"]; 1212 + use crate::comms::locale::VALID_LOCALES; 1213 1213 1214 1214 #[derive(Deserialize)] 1215 1215 #[serde(rename_all = "camelCase")]
+49 -1
src/comms/locale.rs
··· 1 1 pub const DEFAULT_LOCALE: &str = "en"; 2 - pub const VALID_LOCALES: &[&str] = &["en", "zh", "ja", "ko"]; 2 + pub const VALID_LOCALES: &[&str] = &["en", "zh", "ja", "ko", "sv", "fi"]; 3 3 4 4 pub fn validate_locale(locale: &str) -> &str { 5 5 if VALID_LOCALES.contains(&locale) { ··· 37 37 "zh" => &STRINGS_ZH, 38 38 "ja" => &STRINGS_JA, 39 39 "ko" => &STRINGS_KO, 40 + "sv" => &STRINGS_SV, 41 + "fi" => &STRINGS_FI, 40 42 _ => &STRINGS_EN, 41 43 } 42 44 } ··· 131 133 signup_verification_body: "환영합니다! 계정 인증 코드는: {code}\n\n이 코드는 30분 후에 만료됩니다.\n\n{hostname}에서 등록을 완료하려면 이 코드를 입력하세요.", 132 134 legacy_login_subject: "보안 알림: 레거시 로그인 감지 - {hostname}", 133 135 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} 드림", 136 + }; 137 + 138 + static STRINGS_SV: NotificationStrings = NotificationStrings { 139 + welcome_subject: "Välkommen till {hostname}", 140 + welcome_body: "Välkommen till {hostname}!\n\nDitt användarnamn är: @{handle}\n\nTack för att du gick med.", 141 + email_verification_subject: "Verifiera din e-post - {hostname}", 142 + 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.", 143 + password_reset_subject: "Lösenordsåterställning - {hostname}", 144 + 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.", 145 + email_update_subject: "Bekräfta din nya e-post - {hostname}", 146 + 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.", 147 + account_deletion_subject: "Begäran om kontoradering - {hostname}", 148 + 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.", 149 + plc_operation_subject: "{hostname} - PLC-operationstoken", 150 + plc_operation_body: "Hej @{handle},\n\nDu begärde att signera en PLC-operation för ditt konto.\n\nDin verifieringstoken är: {token}\n\nDenna token upphör om 10 minuter.\n\nOm du inte begärde detta kan du säkert ignorera detta meddelande.", 151 + two_factor_code_subject: "Inloggningsverifiering - {hostname}", 152 + two_factor_code_body: "Hej @{handle},\n\nDin inloggningsverifieringskod är: {code}\n\nDenna kod upphör om 10 minuter.\n\nOm du inte begärde detta, skydda ditt konto omedelbart.", 153 + passkey_recovery_subject: "Kontoåterställning - {hostname}", 154 + 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.", 155 + signup_verification_subject: "Verifiera ditt konto - {hostname}", 156 + 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}.", 157 + legacy_login_subject: "Säkerhetsvarning: Äldre inloggning upptäckt - {hostname}", 158 + 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}", 159 + }; 160 + 161 + static STRINGS_FI: NotificationStrings = NotificationStrings { 162 + welcome_subject: "Tervetuloa palveluun {hostname}", 163 + welcome_body: "Tervetuloa palveluun {hostname}!\n\nKäyttäjänimesi on: @{handle}\n\nKiitos liittymisestä.", 164 + email_verification_subject: "Vahvista sähköpostisi - {hostname}", 165 + 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.", 166 + password_reset_subject: "Salasanan palautus - {hostname}", 167 + 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.", 168 + email_update_subject: "Vahvista uusi sähköpostiosoitteesi - {hostname}", 169 + 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.", 170 + account_deletion_subject: "Tilin poistopyyntö - {hostname}", 171 + 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.", 172 + plc_operation_subject: "{hostname} - PLC-toimintotunniste", 173 + plc_operation_body: "Hei @{handle},\n\nPyysit allekirjoittamaan PLC-toiminnon tilillesi.\n\nVahvistustunnisteesi on: {token}\n\nTämä tunniste vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, voit turvallisesti jättää tämän viestin huomiotta.", 174 + two_factor_code_subject: "Kirjautumisen vahvistus - {hostname}", 175 + two_factor_code_body: "Hei @{handle},\n\nKirjautumisen vahvistuskoodisi on: {code}\n\nTämä koodi vanhenee 10 minuutissa.\n\nJos et pyytänyt tätä, suojaa tilisi välittömästi.", 176 + passkey_recovery_subject: "Tilin palautus - {hostname}", 177 + 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.", 178 + signup_verification_subject: "Vahvista tilisi - {hostname}", 179 + 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}.", 180 + legacy_login_subject: "Turvallisuushälytys: Vanha kirjautuminen havaittu - {hostname}", 181 + 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}", 134 182 }; 135 183 136 184 pub fn format_message(template: &str, vars: &[(&str, &str)]) -> String {
+1 -1
src/comms/mod.rs
··· 1 - mod locale; 1 + pub mod locale; 2 2 mod sender; 3 3 mod service; 4 4 mod types;
+26 -1
src/oauth/endpoints/token/grants.rs
··· 169 169 let refresh_token_str = request 170 170 .refresh_token 171 171 .ok_or_else(|| OAuthError::InvalidRequest("refresh_token is required".to_string()))?; 172 + tracing::info!( 173 + refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 174 + has_dpop = dpop_proof.is_some(), 175 + "Refresh token grant requested" 176 + ); 172 177 if let Some(token_id) = db::check_refresh_token_used(&state.db, &refresh_token_str).await? { 178 + tracing::warn!( 179 + refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 180 + "Refresh token reuse detected, revoking token family" 181 + ); 173 182 db::delete_token_family(&state.db, token_id).await?; 174 183 return Err(OAuthError::InvalidGrant( 175 184 "Refresh token reuse detected, token family revoked".to_string(), ··· 177 186 } 178 187 let (db_id, token_data) = db::get_token_by_refresh_token(&state.db, &refresh_token_str) 179 188 .await? 180 - .ok_or_else(|| OAuthError::InvalidGrant("Invalid refresh token".to_string()))?; 189 + .ok_or_else(|| { 190 + tracing::warn!( 191 + refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())], 192 + "Refresh token not found in database" 193 + ); 194 + OAuthError::InvalidGrant("Invalid refresh token".to_string()) 195 + })?; 181 196 if token_data.expires_at < Utc::now() { 197 + tracing::warn!( 198 + did = %token_data.did, 199 + expired_at = %token_data.expires_at, 200 + "Refresh token has expired" 201 + ); 182 202 db::delete_token_family(&state.db, db_id).await?; 183 203 return Err(OAuthError::InvalidGrant( 184 204 "Refresh token has expired".to_string(), ··· 227 247 new_expires_at, 228 248 ) 229 249 .await?; 250 + tracing::info!( 251 + did = %token_data.did, 252 + new_expires_at = %new_expires_at, 253 + "Refresh token rotated successfully" 254 + ); 230 255 let access_token = create_access_token( 231 256 &new_token_id.0, 232 257 &token_data.did,