+22
-7
frontend/src/components/ReauthModal.svelte
+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">×</button>
+14
-1
frontend/src/lib/api.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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}>×</button>
-6
frontend/src/routes/OAuthPasskey.svelte
-6
frontend/src/routes/OAuthPasskey.svelte
+8
-2
frontend/src/routes/Security.svelte
+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
+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
+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
+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
+1
-1
src/api/server/session.rs
+49
-1
src/comms/locale.rs
+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
-1
src/comms/mod.rs
+26
-1
src/oauth/endpoints/token/grants.rs
+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,