···463463}
464464465465#[tokio::test]
466466-async fn test_update_email_taken_by_another_user() {
466466+async fn test_update_email_to_same_as_another_user_allowed() {
467467 let client = common::client();
468468 let base_url = common::base_url().await;
469469 let pool = common::get_test_db_pool().await;
···499499 .send()
500500 .await
501501 .expect("Failed to update email");
502502- assert_eq!(res.status(), StatusCode::BAD_REQUEST);
503503- let body: Value = res.json().await.expect("Invalid JSON");
504504- assert_eq!(body["error"], "InvalidRequest");
505505- assert!(
506506- body["message"]
507507- .as_str()
508508- .unwrap_or("")
509509- .contains("already in use")
502502+ assert_eq!(
503503+ res.status(),
504504+ StatusCode::OK,
505505+ "Multiple accounts can share the same email address"
510506 );
507507+508508+ let user_email: Option<String> =
509509+ sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did2)
510510+ .fetch_one(pool)
511511+ .await
512512+ .expect("User not found");
513513+ assert_eq!(user_email, Some(email1.clone()));
511514}
+409
crates/tranquil-pds/tests/oauth.rs
···12371237 "Should require lxm parameter for granular scopes"
12381238 );
12391239}
12401240+12411241+#[tokio::test]
12421242+async fn test_oauth_metadata_includes_prompt_values_supported() {
12431243+ let url = base_url().await;
12441244+ let client = client();
12451245+ let as_res = client
12461246+ .get(format!("{}/.well-known/oauth-authorization-server", url))
12471247+ .send()
12481248+ .await
12491249+ .unwrap();
12501250+ assert_eq!(as_res.status(), StatusCode::OK);
12511251+ let as_body: Value = as_res.json().await.unwrap();
12521252+ let prompt_values = as_body["prompt_values_supported"]
12531253+ .as_array()
12541254+ .expect("prompt_values_supported should be an array");
12551255+ assert!(
12561256+ prompt_values.contains(&json!("none")),
12571257+ "Should support prompt=none"
12581258+ );
12591259+ assert!(
12601260+ prompt_values.contains(&json!("login")),
12611261+ "Should support prompt=login"
12621262+ );
12631263+ assert!(
12641264+ prompt_values.contains(&json!("consent")),
12651265+ "Should support prompt=consent"
12661266+ );
12671267+ assert!(
12681268+ prompt_values.contains(&json!("select_account")),
12691269+ "Should support prompt=select_account"
12701270+ );
12711271+ assert!(
12721272+ prompt_values.contains(&json!("create")),
12731273+ "Should support prompt=create"
12741274+ );
12751275+}
12761276+12771277+#[tokio::test]
12781278+async fn test_par_accepts_valid_prompt_values() {
12791279+ let url = base_url().await;
12801280+ let client = client();
12811281+ let redirect_uri = "https://example.com/callback";
12821282+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
12831283+ let client_id = mock_client.uri();
12841284+ let (_, code_challenge) = generate_pkce();
12851285+ let valid_prompts = ["none", "login", "consent", "select_account", "create"];
12861286+ for prompt in valid_prompts {
12871287+ let par_res = client
12881288+ .post(format!("{}/oauth/par", url))
12891289+ .form(&[
12901290+ ("response_type", "code"),
12911291+ ("client_id", &client_id),
12921292+ ("redirect_uri", redirect_uri),
12931293+ ("code_challenge", &code_challenge),
12941294+ ("code_challenge_method", "S256"),
12951295+ ("scope", "atproto"),
12961296+ ("state", "test-state"),
12971297+ ("prompt", prompt),
12981298+ ])
12991299+ .send()
13001300+ .await
13011301+ .unwrap();
13021302+ assert_eq!(
13031303+ par_res.status(),
13041304+ StatusCode::CREATED,
13051305+ "PAR should accept prompt={}",
13061306+ prompt
13071307+ );
13081308+ }
13091309+}
13101310+13111311+#[tokio::test]
13121312+async fn test_par_rejects_invalid_prompt_value() {
13131313+ let url = base_url().await;
13141314+ let client = client();
13151315+ let redirect_uri = "https://example.com/callback";
13161316+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
13171317+ let client_id = mock_client.uri();
13181318+ let (_, code_challenge) = generate_pkce();
13191319+ let par_res = client
13201320+ .post(format!("{}/oauth/par", url))
13211321+ .form(&[
13221322+ ("response_type", "code"),
13231323+ ("client_id", &client_id),
13241324+ ("redirect_uri", redirect_uri),
13251325+ ("code_challenge", &code_challenge),
13261326+ ("code_challenge_method", "S256"),
13271327+ ("scope", "atproto"),
13281328+ ("state", "test-state"),
13291329+ ("prompt", "invalid_prompt"),
13301330+ ])
13311331+ .send()
13321332+ .await
13331333+ .unwrap();
13341334+ assert_eq!(
13351335+ par_res.status(),
13361336+ StatusCode::BAD_REQUEST,
13371337+ "PAR should reject invalid prompt value"
13381338+ );
13391339+ let body: Value = par_res.json().await.unwrap();
13401340+ assert_eq!(body["error"], "invalid_request");
13411341+ assert!(
13421342+ body["error_description"]
13431343+ .as_str()
13441344+ .unwrap_or("")
13451345+ .contains("prompt"),
13461346+ "Error should mention prompt"
13471347+ );
13481348+}
13491349+13501350+#[tokio::test]
13511351+async fn test_prompt_create_redirects_to_register() {
13521352+ let url = base_url().await;
13531353+ let client = no_redirect_client();
13541354+ let redirect_uri = "https://example.com/callback";
13551355+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
13561356+ let client_id = mock_client.uri();
13571357+ let (_, code_challenge) = generate_pkce();
13581358+ let par_res = reqwest::Client::new()
13591359+ .post(format!("{}/oauth/par", url))
13601360+ .form(&[
13611361+ ("response_type", "code"),
13621362+ ("client_id", &client_id),
13631363+ ("redirect_uri", redirect_uri),
13641364+ ("code_challenge", &code_challenge),
13651365+ ("code_challenge_method", "S256"),
13661366+ ("scope", "atproto"),
13671367+ ("state", "test-state"),
13681368+ ("prompt", "create"),
13691369+ ])
13701370+ .send()
13711371+ .await
13721372+ .unwrap();
13731373+ assert_eq!(par_res.status(), StatusCode::CREATED);
13741374+ let par_body: Value = par_res.json().await.unwrap();
13751375+ let request_uri = par_body["request_uri"].as_str().unwrap();
13761376+ let auth_res = client
13771377+ .get(format!("{}/oauth/authorize", url))
13781378+ .query(&[("request_uri", request_uri)])
13791379+ .send()
13801380+ .await
13811381+ .unwrap();
13821382+ assert!(
13831383+ auth_res.status().is_redirection(),
13841384+ "Should redirect when prompt=create"
13851385+ );
13861386+ let location = auth_res
13871387+ .headers()
13881388+ .get("location")
13891389+ .expect("Should have Location header")
13901390+ .to_str()
13911391+ .unwrap();
13921392+ assert!(
13931393+ location.contains("/app/oauth/register"),
13941394+ "Should redirect to /app/oauth/register, got: {}",
13951395+ location
13961396+ );
13971397+ assert!(
13981398+ location.contains("request_uri="),
13991399+ "Should include request_uri in redirect"
14001400+ );
14011401+}
14021402+14031403+#[tokio::test]
14041404+async fn test_register_complete_rejects_invalid_request_uri() {
14051405+ let url = base_url().await;
14061406+ let client = client();
14071407+ let res = client
14081408+ .post(format!("{}/oauth/register/complete", url))
14091409+ .json(&json!({
14101410+ "request_uri": "urn:ietf:params:oauth:request_uri:nonexistent",
14111411+ "did": "did:plc:test123",
14121412+ "app_password": "test-password"
14131413+ }))
14141414+ .send()
14151415+ .await
14161416+ .unwrap();
14171417+ assert_eq!(
14181418+ res.status(),
14191419+ StatusCode::BAD_REQUEST,
14201420+ "Should reject invalid request_uri"
14211421+ );
14221422+ let body: Value = res.json().await.unwrap();
14231423+ assert_eq!(body["error"], "invalid_request");
14241424+}
14251425+14261426+#[tokio::test]
14271427+async fn test_register_complete_rejects_wrong_credentials() {
14281428+ let url = base_url().await;
14291429+ let http_client = client();
14301430+ let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
14311431+ let handle = format!("rc{}", suffix);
14321432+ let email = format!("rc{}@example.com", suffix);
14331433+ let password = "Regcomplete123!";
14341434+ let create_res = http_client
14351435+ .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
14361436+ .json(&json!({ "handle": handle, "email": email, "password": password }))
14371437+ .send()
14381438+ .await
14391439+ .unwrap();
14401440+ assert_eq!(create_res.status(), StatusCode::OK);
14411441+ let account: Value = create_res.json().await.unwrap();
14421442+ let user_did = account["did"].as_str().unwrap();
14431443+ verify_new_account(&http_client, user_did).await;
14441444+ let redirect_uri = "https://example.com/callback";
14451445+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
14461446+ let client_id = mock_client.uri();
14471447+ let (_, code_challenge) = generate_pkce();
14481448+ let par_res = http_client
14491449+ .post(format!("{}/oauth/par", url))
14501450+ .form(&[
14511451+ ("response_type", "code"),
14521452+ ("client_id", &client_id),
14531453+ ("redirect_uri", redirect_uri),
14541454+ ("code_challenge", &code_challenge),
14551455+ ("code_challenge_method", "S256"),
14561456+ ("scope", "atproto"),
14571457+ ("state", "test-state"),
14581458+ ("prompt", "create"),
14591459+ ])
14601460+ .send()
14611461+ .await
14621462+ .unwrap();
14631463+ let par_body: Value = par_res.json().await.unwrap();
14641464+ let request_uri = par_body["request_uri"].as_str().unwrap();
14651465+ let res = http_client
14661466+ .post(format!("{}/oauth/register/complete", url))
14671467+ .json(&json!({
14681468+ "request_uri": request_uri,
14691469+ "did": user_did,
14701470+ "app_password": "wrong-password"
14711471+ }))
14721472+ .send()
14731473+ .await
14741474+ .unwrap();
14751475+ assert_eq!(
14761476+ res.status(),
14771477+ StatusCode::FORBIDDEN,
14781478+ "Should reject wrong credentials"
14791479+ );
14801480+ let body: Value = res.json().await.unwrap();
14811481+ assert_eq!(body["error"], "access_denied");
14821482+}
14831483+14841484+#[tokio::test]
14851485+async fn test_full_oauth_registration_flow() {
14861486+ let url = base_url().await;
14871487+ let http_client = client();
14881488+14891489+ let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
14901490+ let handle = format!("oauthreg{}", suffix);
14911491+ let email = format!("oauthreg{}@example.com", suffix);
14921492+ let password = "OauthRegTest123!";
14931493+14941494+ let redirect_uri = "https://example.com/callback";
14951495+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
14961496+ let client_id = mock_client.uri();
14971497+ let (code_verifier, code_challenge) = generate_pkce();
14981498+ let state = format!("state-{}", suffix);
14991499+15001500+ let par_res = http_client
15011501+ .post(format!("{}/oauth/par", url))
15021502+ .form(&[
15031503+ ("response_type", "code"),
15041504+ ("client_id", &client_id),
15051505+ ("redirect_uri", redirect_uri),
15061506+ ("code_challenge", &code_challenge),
15071507+ ("code_challenge_method", "S256"),
15081508+ ("scope", "atproto"),
15091509+ ("state", &state),
15101510+ ("prompt", "create"),
15111511+ ])
15121512+ .send()
15131513+ .await
15141514+ .unwrap();
15151515+ assert_eq!(
15161516+ par_res.status(),
15171517+ StatusCode::CREATED,
15181518+ "PAR with prompt=create should succeed"
15191519+ );
15201520+ let par_body: Value = par_res.json().await.unwrap();
15211521+ let request_uri = par_body["request_uri"].as_str().unwrap();
15221522+15231523+ let create_res = http_client
15241524+ .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
15251525+ .json(&json!({ "handle": handle, "email": email, "password": password }))
15261526+ .send()
15271527+ .await
15281528+ .unwrap();
15291529+ assert_eq!(
15301530+ create_res.status(),
15311531+ StatusCode::OK,
15321532+ "Account creation should succeed"
15331533+ );
15341534+ let account: Value = create_res.json().await.unwrap();
15351535+ let user_did = account["did"].as_str().unwrap();
15361536+ let access_jwt = account["accessJwt"].as_str().unwrap();
15371537+15381538+ let app_password_res = http_client
15391539+ .post(format!(
15401540+ "{}/xrpc/com.atproto.server.createAppPassword",
15411541+ url
15421542+ ))
15431543+ .header("Authorization", format!("Bearer {}", access_jwt))
15441544+ .json(&json!({ "name": "oauth-test-app" }))
15451545+ .send()
15461546+ .await
15471547+ .unwrap();
15481548+ assert_eq!(
15491549+ app_password_res.status(),
15501550+ StatusCode::OK,
15511551+ "App password creation should succeed"
15521552+ );
15531553+ let app_password_body: Value = app_password_res.json().await.unwrap();
15541554+ let app_password = app_password_body["password"].as_str().unwrap();
15551555+15561556+ verify_new_account(&http_client, user_did).await;
15571557+15581558+ let complete_res = http_client
15591559+ .post(format!("{}/oauth/register/complete", url))
15601560+ .json(&json!({
15611561+ "request_uri": request_uri,
15621562+ "did": user_did,
15631563+ "app_password": app_password
15641564+ }))
15651565+ .send()
15661566+ .await
15671567+ .unwrap();
15681568+ assert_eq!(
15691569+ complete_res.status(),
15701570+ StatusCode::OK,
15711571+ "register_complete should succeed"
15721572+ );
15731573+ let complete_body: Value = complete_res.json().await.unwrap();
15741574+ let mut redirect_location = complete_body["redirect_uri"]
15751575+ .as_str()
15761576+ .expect("Expected redirect_uri from register_complete")
15771577+ .to_string();
15781578+15791579+ if redirect_location.contains("/oauth/consent") {
15801580+ let consent_res = http_client
15811581+ .post(format!("{}/oauth/authorize/consent", url))
15821582+ .header("Content-Type", "application/json")
15831583+ .json(&json!({
15841584+ "request_uri": request_uri,
15851585+ "approved_scopes": ["atproto"],
15861586+ "remember": false
15871587+ }))
15881588+ .send()
15891589+ .await
15901590+ .unwrap();
15911591+ assert_eq!(
15921592+ consent_res.status(),
15931593+ StatusCode::OK,
15941594+ "Consent should succeed"
15951595+ );
15961596+ let consent_body: Value = consent_res.json().await.unwrap();
15971597+ redirect_location = consent_body["redirect_uri"]
15981598+ .as_str()
15991599+ .expect("Expected redirect_uri from consent")
16001600+ .to_string();
16011601+ }
16021602+16031603+ assert!(
16041604+ redirect_location.contains("code="),
16051605+ "Should have authorization code in redirect: {}",
16061606+ redirect_location
16071607+ );
16081608+16091609+ let code = redirect_location
16101610+ .split("code=")
16111611+ .nth(1)
16121612+ .unwrap()
16131613+ .split('&')
16141614+ .next()
16151615+ .unwrap();
16161616+16171617+ let token_res = http_client
16181618+ .post(format!("{}/oauth/token", url))
16191619+ .form(&[
16201620+ ("grant_type", "authorization_code"),
16211621+ ("code", code),
16221622+ ("redirect_uri", redirect_uri),
16231623+ ("code_verifier", &code_verifier),
16241624+ ("client_id", &client_id),
16251625+ ])
16261626+ .send()
16271627+ .await
16281628+ .unwrap();
16291629+ assert_eq!(
16301630+ token_res.status(),
16311631+ StatusCode::OK,
16321632+ "Token exchange should succeed"
16331633+ );
16341634+ let token_body: Value = token_res.json().await.unwrap();
16351635+ assert!(
16361636+ token_body["access_token"].is_string(),
16371637+ "Should have access_token"
16381638+ );
16391639+ assert!(
16401640+ token_body["refresh_token"].is_string(),
16411641+ "Should have refresh_token"
16421642+ );
16431643+ assert_eq!(token_body["token_type"], "Bearer");
16441644+ assert_eq!(
16451645+ token_body["sub"], user_did,
16461646+ "Token sub should match user DID"
16471647+ );
16481648+}
+1-1
crates/tranquil-pds/tests/sso.rs
···1039103910401040 let redirect_url = body["redirectUrl"].as_str().unwrap();
10411041 assert!(
10421042- redirect_url.contains("/app/oauth/verify"),
10421042+ redirect_url.contains("/app/verify"),
10431043 "Non-auto-verified channel should redirect to verify, got: {}",
10441044 redirect_url
10451045 );
+3-3
frontend/public/homepage.html
···461461 <div class="feature">
462462 <h3>Real security</h3>
463463 <p>
464464- Sign in with passkeys or SSO, add two-factor authentication,
465465- set up backup codes, and mark devices you trust. Your account
466466- stays yours.
464464+ Sign in with passkeys or SSO, add two-factor authentication, set
465465+ up backup codes, and mark devices you trust. Your account stays
466466+ yours.
467467 </p>
468468 </div>
469469
+12-10
frontend/src/App.svelte
···66 import { isLoading as i18nLoading } from 'svelte-i18n'
77 import Toast from './components/Toast.svelte'
88 import Login from './routes/Login.svelte'
99- import Register from './routes/Register.svelte'
1010- import RegisterPasskey from './routes/RegisterPasskey.svelte'
119 import RegisterSso from './routes/RegisterSso.svelte'
1210 import Verify from './routes/Verify.svelte'
1311 import ResetPassword from './routes/ResetPassword.svelte'
···2927 import OAuthPasskey from './routes/OAuthPasskey.svelte'
3028 import OAuthDelegation from './routes/OAuthDelegation.svelte'
3129 import OAuthError from './routes/OAuthError.svelte'
3232- import OAuthSsoRegister from './routes/OAuthSsoRegister.svelte'
3030+ import SsoRegisterComplete from './routes/SsoRegisterComplete.svelte'
3131+ import Register from './routes/Register.svelte'
3232+ import RegisterPassword from './routes/RegisterPassword.svelte'
3333 import Security from './routes/Security.svelte'
3434 import TrustedDevices from './routes/TrustedDevices.svelte'
3535 import Controllers from './routes/Controllers.svelte'
···9898 switch (path) {
9999 case '/login':
100100 return Login
101101- case '/register':
102102- return RegisterPasskey
103103- case '/register-password':
104104- return Register
105105- case '/register-sso':
106106- return RegisterSso
107101 case '/verify':
108102 return Verify
109103 case '/reset-password':
···145139 case '/oauth/error':
146140 return OAuthError
147141 case '/oauth/sso-register':
148148- return OAuthSsoRegister
142142+ return SsoRegisterComplete
143143+ case '/register':
144144+ case '/oauth/register':
145145+ return Register
146146+ case '/oauth/register-sso':
147147+ return RegisterSso
148148+ case '/oauth/register-password':
149149+ return RegisterPassword
149150 case '/security':
150151 return Security
151152 case '/trusted-devices':
···167168168169 let currentPath = $derived(getCurrentPath())
169170 let CurrentComponent = $derived(getComponent(currentPath))
171171+170172</script>
171173172174<main>
···11-export * from "./types";
22-export * from "./atproto-client";
33-export * from "./storage";
44-export * from "./blob-migration";
11+export * from "./types.ts";
22+export * from "./atproto-client.ts";
33+export * from "./storage.ts";
44+export * from "./blob-migration.ts";
55export {
66 createInboundMigrationFlow,
77 type InboundMigrationFlow,
88-} from "./flow.svelte";
88+} from "./flow.svelte.ts";
99export {
1010 clearOfflineState,
1111 createOfflineInboundMigrationFlow,
1212 getOfflineResumeInfo,
1313 hasPendingOfflineMigration,
1414-} from "./offline-flow.svelte";
1515-export type { OfflineInboundMigrationFlow } from "./offline-flow.svelte";
1414+} from "./offline-flow.svelte.ts";
1515+export type { OfflineInboundMigrationFlow } from "./offline-flow.svelte.ts";
···11-export * from "./types";
22-export * from "./flow.svelte";
11+export * from "./types.ts";
22+export * from "./flow.svelte.ts";
33export { default as VerificationStep } from "./VerificationStep.svelte";
44export { default as KeyChoiceStep } from "./KeyChoiceStep.svelte";
55export { default as DidDocStep } from "./DidDocStep.svelte";
+1-1
frontend/src/lib/serverConfig.svelte.ts
···11-import { api } from "./api";
11+import { api } from "./api.ts";
2233interface ServerConfigState {
44 serverName: string | null;
···150150 "email": "Email",
151151 "emailAddress": "Email Address",
152152 "emailPlaceholder": "you@example.com",
153153+ "emailInUseWarning": "This email is already associated with another account. You can still use it, but for account recovery you may need to use your handle instead.",
153154 "discord": "Discord",
154155 "discordId": "Discord User ID",
155156 "discordIdPlaceholder": "Your Discord user ID",
156157 "discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)",
158158+ "discordInUseWarning": "This Discord ID is already associated with another account.",
157159 "telegram": "Telegram",
158160 "telegramUsername": "Telegram Username",
159161 "telegramUsernamePlaceholder": "@yourusername",
162162+ "telegramInUseWarning": "This Telegram username is already associated with another account.",
160163 "signal": "Signal",
161164 "signalNumber": "Signal Phone Number",
162165 "signalNumberPlaceholder": "+1234567890",
163166 "signalNumberHint": "Include country code (eg., +1 for US)",
167167+ "signalInUseWarning": "This Signal number is already associated with another account.",
164168 "notConfigured": "not configured",
165169 "inviteCode": "Invite Code",
166170 "inviteCodePlaceholder": "Enter your invite code",
···275279 "currentEmail": "Current: {email}",
276280 "newEmail": "New Email",
277281 "newEmailPlaceholder": "new@example.com",
282282+ "emailInUseWarning": "This email is already used by another account. You can still use it, but account recovery may require your handle.",
278283 "changeEmailButton": "Change Email",
279284 "requesting": "Requesting...",
280285 "verificationCode": "Verification Code",
···437442 "noCodes": "No invite codes yet",
438443 "available": "Available",
439444 "used": "Used by @{handle}",
445445+ "spent": "Spent",
440446 "disabled": "Disabled",
441447 "usedBy": "Used by",
442448 "disableConfirm": "Disable this invite code? It can no longer be used.",
···577583 "hideHistory": "Hide History",
578584 "noMessages": "No messages found.",
579585 "sent": "sent",
580580- "failed": "failed"
586586+ "failed": "failed",
587587+ "discordInUseWarning": "This Discord ID is already associated with another account.",
588588+ "telegramInUseWarning": "This Telegram username is already associated with another account.",
589589+ "signalInUseWarning": "This Signal number is already associated with another account."
581590 },
582591 "repoExplorer": {
583592 "title": "Repository Explorer",
···777786 "subtitle": "Select an account to continue",
778787 "useAnother": "Use a different account"
779788 },
789789+ "register": {
790790+ "title": "Create Account",
791791+ "subtitle": "Create an account to continue to",
792792+ "subtitleGeneric": "Create an account to continue",
793793+ "haveAccount": "Already have an account? Sign in"
794794+ },
780795 "twoFactor": {
781796 "title": "Two-Factor Authentication",
782797 "subtitle": "Additional verification is required",
···887902 "sendCode": "Send Reset Code",
888903 "sending": "Sending...",
889904 "codeSent": "Password reset code sent! Check your preferred notification channel.",
905905+ "multipleAccountsWarning": "Multiple accounts share this email. The reset code was sent to the most recently created account. Use your handle instead for a specific account.",
890906 "enterCode": "Enter the code you received and your new password.",
891907 "code": "Reset Code",
892908 "codePlaceholder": "Enter reset code",
+24-2
frontend/src/locales/fi.json
···150150 "email": "Sähköposti",
151151 "emailAddress": "Sähköpostiosoite",
152152 "emailPlaceholder": "sinä@esimerkki.fi",
153153+ "emailInUseWarning": "Tämä sähköposti on jo yhdistetty toiseen tiliin. Voit silti käyttää sitä, mutta tilin palauttamiseen saatat joutua käyttämään käsittelynimeäsi.",
153154 "discord": "Discord",
154155 "discordId": "Discord-käyttäjätunnus",
155156 "discordIdPlaceholder": "Discord-käyttäjätunnuksesi",
156157 "discordIdHint": "Numeerinen Discord-käyttäjätunnuksesi (ota Kehittäjätila käyttöön löytääksesi sen)",
158158+ "discordInUseWarning": "Tämä Discord-tunnus on jo yhdistetty toiseen tiliin.",
157159 "telegram": "Telegram",
158160 "telegramUsername": "Telegram-käyttäjänimi",
159161 "telegramUsernamePlaceholder": "@käyttäjänimesi",
162162+ "telegramInUseWarning": "Tämä Telegram-käyttäjänimi on jo yhdistetty toiseen tiliin.",
160163 "signal": "Signal",
161164 "signalNumber": "Signal-puhelinnumero",
162165 "signalNumberPlaceholder": "+358401234567",
163166 "signalNumberHint": "Sisällytä maakoodi (esim. +358 Suomelle)",
167167+ "signalInUseWarning": "Tämä Signal-numero on jo yhdistetty toiseen tiliin.",
164168 "notConfigured": "ei määritetty",
165169 "inviteCode": "Kutsukoodi",
166170 "inviteCodePlaceholder": "Syötä kutsukoodisi",
···275279 "currentEmail": "Nykyinen: {email}",
276280 "newEmail": "Uusi sähköposti",
277281 "newEmailPlaceholder": "uusi@esimerkki.fi",
282282+ "emailInUseWarning": "Tämä sähköposti on jo toisen tilin käytössä. Voit silti käyttää sitä, mutta tilin palauttaminen voi vaatia käsittelynimeäsi.",
278283 "changeEmailButton": "Vaihda sähköposti",
279284 "requesting": "Pyydetään...",
280285 "verificationCode": "Vahvistuskoodi",
···437442 "noCodes": "Ei vielä kutsukoodeja",
438443 "available": "Saatavilla",
439444 "used": "Käyttänyt @{handle}",
445445+ "spent": "Käytetty",
440446 "disabled": "Poistettu käytöstä",
441447 "usedBy": "Käyttänyt",
442448 "disableConfirm": "Poista tämä kutsukoodi käytöstä? Sitä ei voi enää käyttää.",
···577583 "hideHistory": "Piilota historia",
578584 "noMessages": "Viestejä ei löytynyt.",
579585 "sent": "lähetetty",
580580- "failed": "epäonnistui"
586586+ "failed": "epäonnistui",
587587+ "discordInUseWarning": "Tämä Discord-tunnus on jo yhdistetty toiseen tiliin.",
588588+ "telegramInUseWarning": "Tämä Telegram-käyttäjänimi on jo yhdistetty toiseen tiliin.",
589589+ "signalInUseWarning": "Tämä Signal-numero on jo yhdistetty toiseen tiliin."
581590 },
582591 "repoExplorer": {
583592 "title": "Tietovarastoselaaja",
···696705 "orContinueWith": "Tai jatka käyttäen",
697706 "orUseCredentials": "Tai kirjaudu tunnuksilla"
698707 },
708708+ "register": {
709709+ "title": "Luo tili",
710710+ "subtitle": "Luo tili jatkaaksesi sovellukseen",
711711+ "subtitleGeneric": "Luo tili jatkaaksesi",
712712+ "haveAccount": "Onko sinulla jo tili? Kirjaudu sisään"
713713+ },
699714 "sso": {
700715 "linkedAccounts": "Linkitetyt tilit",
701716 "linkedAccountsDesc": "Ulkoiset tilit, jotka on linkitetty identiteettiisi kertakirjautumista varten.",
···829844 "error_expired": "Rekisteröintisessio on vanhentunut. Yritä uudelleen.",
830845 "error_handle_required": "Valitse käsittelynimi",
831846 "emailVerifiedByProvider": "Tämä sähköposti on vahvistettu {provider} kautta. Lisävahvistusta ei tarvita.",
832832- "emailChangedNeedsVerification": "Jos käytät eri sähköpostia, sinun täytyy vahvistaa se."
847847+ "emailChangedNeedsVerification": "Jos käytät eri sähköpostia, sinun täytyy vahvistaa se.",
848848+ "infoAfterTitle": "Tilin luomisen jälkeen",
849849+ "infoAddPassword": "Lisää salasana perinteistä kirjautumista varten",
850850+ "infoAddPasskey": "Määritä pääsyavain salasanattomaan kirjautumiseen",
851851+ "infoLinkProviders": "Linkitä lisää SSO-palveluntarjoajia",
852852+ "infoChangeHandle": "Vaihda käsittelynimesi tai käytä omaa verkkotunnusta",
853853+ "tryAgain": "Yritä uudelleen"
833854 },
834855 "verify": {
835856 "title": "Vahvista tilisi",
···881902 "sendCode": "Lähetä palautuskoodi",
882903 "sending": "Lähetetään...",
883904 "codeSent": "Palautuskoodi lähetetty! Tarkista ensisijainen ilmoituskanavasi.",
905905+ "multipleAccountsWarning": "Useampi tili käyttää tätä sähköpostia. Palautuskoodi lähetettiin viimeksi luodulle tilille. Käytä käsittelynimeäsi tietylle tilille.",
884906 "enterCode": "Syötä saamasi koodi ja uusi salasanasi.",
885907 "code": "Palautuskoodi",
886908 "codePlaceholder": "Syötä palautuskoodi",
···143143 "email": "이메일",
144144 "emailAddress": "이메일 주소",
145145 "emailPlaceholder": "you@example.com",
146146+ "emailInUseWarning": "이 이메일은 이미 다른 계정과 연결되어 있습니다. 계속 사용할 수 있지만, 계정 복구 시 핸들이 필요할 수 있습니다.",
146147 "discord": "Discord",
147148 "discordId": "Discord 사용자 ID",
148149 "discordIdPlaceholder": "Discord 사용자 ID",
149150 "discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
151151+ "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.",
150152 "telegram": "Telegram",
151153 "telegramUsername": "Telegram 사용자 이름",
152154 "telegramUsernamePlaceholder": "@yourusername",
155155+ "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.",
153156 "signal": "Signal",
154157 "signalNumber": "Signal 전화번호",
155158 "signalNumberPlaceholder": "+821012345678",
156159 "signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
160160+ "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다.",
157161 "notConfigured": "구성되지 않음",
158162 "inviteCode": "초대 코드",
159163 "inviteCodePlaceholder": "초대 코드 입력",
···269273 "newEmail": "새 이메일",
270274 "newEmailPlaceholder": "new@example.com",
271275 "changeEmailButton": "이메일 변경",
276276+ "emailInUseWarning": "이 이메일은 이미 다른 계정과 연결되어 있습니다. 계속 사용하실 수 있지만, 계정 복구 시 이메일 대신 핸들을 사용해야 할 수 있습니다.",
272277 "requesting": "요청 중...",
273278 "verificationCode": "인증 코드",
274279 "verificationCodePlaceholder": "인증 코드 입력",
···430435 "noCodes": "초대 코드가 아직 없습니다",
431436 "available": "사용 가능",
432437 "used": "@{handle}이(가) 사용함",
438438+ "spent": "소진됨",
433439 "disabled": "비활성화됨",
434440 "usedBy": "사용자",
435441 "disableConfirm": "이 초대 코드를 비활성화하시겠습니까? 더 이상 사용할 수 없습니다.",
···570576 "hideHistory": "기록 숨기기",
571577 "noMessages": "메시지가 없습니다.",
572578 "sent": "전송됨",
573573- "failed": "실패"
579579+ "failed": "실패",
580580+ "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.",
581581+ "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.",
582582+ "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다."
574583 },
575584 "repoExplorer": {
576585 "title": "저장소 탐색기",
···689698 "orContinueWith": "또는 다음으로 계속",
690699 "orUseCredentials": "또는 자격 증명으로 로그인"
691700 },
701701+ "register": {
702702+ "title": "계정 만들기",
703703+ "subtitle": "계속하려면 계정을 만드세요",
704704+ "subtitleGeneric": "계속하려면 계정을 만드세요",
705705+ "haveAccount": "이미 계정이 있으신가요? 로그인"
706706+ },
692707 "sso": {
693708 "linkedAccounts": "연결된 계정",
694709 "linkedAccountsDesc": "싱글 사인온을 위해 연결된 외부 계정입니다.",
···822837 "error_expired": "등록 세션이 만료되었습니다. 다시 시도해 주세요.",
823838 "error_handle_required": "핸들을 선택해 주세요",
824839 "emailVerifiedByProvider": "이 이메일은 {provider}에서 인증되었습니다. 추가 인증이 필요하지 않습니다.",
825825- "emailChangedNeedsVerification": "다른 이메일을 사용하시면 인증이 필요합니다."
840840+ "emailChangedNeedsVerification": "다른 이메일을 사용하시면 인증이 필요합니다.",
841841+ "infoAfterTitle": "계정 생성 후",
842842+ "infoAddPassword": "기존 로그인을 위한 비밀번호 추가",
843843+ "infoAddPasskey": "비밀번호 없는 로그인을 위한 패스키 설정",
844844+ "infoLinkProviders": "추가 SSO 제공자 연결",
845845+ "infoChangeHandle": "핸들 변경 또는 사용자 정의 도메인 사용",
846846+ "tryAgain": "다시 시도"
826847 },
827848 "verify": {
828849 "title": "계정 인증",
···886907 "success": "비밀번호가 재설정되었습니다!",
887908 "requestNewCode": "새 코드 요청",
888909 "passwordsMismatch": "비밀번호가 일치하지 않습니다",
889889- "passwordLength": "비밀번호는 8자 이상이어야 합니다"
910910+ "passwordLength": "비밀번호는 8자 이상이어야 합니다",
911911+ "multipleAccountsWarning": "여러 계정에서 이 이메일을 공유하고 있습니다. 재설정 코드는 가장 최근에 생성된 계정으로 전송되었습니다. 특정 계정을 복구하려면 핸들을 사용하세요."
890912 },
891913 "recoverPasskey": {
892914 "title": "계정 복구",
+26-4
frontend/src/locales/sv.json
···147147 "discordId": "Discord användar-ID",
148148 "discordIdPlaceholder": "Ditt Discord användar-ID",
149149 "discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
150150+ "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.",
150151 "telegram": "Telegram",
151152 "telegramUsername": "Telegram-användarnamn",
152153 "telegramUsernamePlaceholder": "@dittanvändarnamn",
154154+ "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.",
153155 "signal": "Signal",
154156 "signalNumber": "Signal-telefonnummer",
155157 "signalNumberPlaceholder": "+46701234567",
156158 "signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
159159+ "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto.",
157160 "notConfigured": "ej konfigurerad",
158161 "inviteCode": "Inbjudningskod",
159162 "inviteCodePlaceholder": "Ange din inbjudningskod",
···161164 "createButton": "Skapa konto",
162165 "alreadyHaveAccount": "Har du redan ett konto?",
163166 "signIn": "Logga in",
167167+ "emailInUseWarning": "Denna e-post är redan kopplad till ett annat konto. Du kan fortfarande använda den, men för kontoåterställning kan du behöva använda ditt användarnamn istället.",
164168 "passkeyAccount": "Nyckel",
165169 "passwordAccount": "Lösenord",
166170 "ssoAccount": "SSO",
···269273 "newEmail": "Ny e-post",
270274 "newEmailPlaceholder": "ny@exempel.se",
271275 "changeEmailButton": "Ändra e-post",
276276+ "emailInUseWarning": "Denna e-post används redan av ett annat konto. Du kan fortfarande använda den, men kontoåterställning kan kräva ditt användarnamn.",
272277 "requesting": "Begär...",
273278 "verificationCode": "Verifieringskod",
274279 "verificationCodePlaceholder": "Ange verifieringskod",
···435440 "disableConfirm": "Inaktivera denna inbjudningskod? Den kan inte längre användas.",
436441 "created": "Inbjudningskod skapad",
437442 "copy": "Kopiera",
438438- "createdOn": "Skapad {date}"
443443+ "createdOn": "Skapad {date}",
444444+ "spent": "Förbrukad"
439445 },
440446 "security": {
441447 "title": "Säkerhet",
···570576 "hideHistory": "Dölj historik",
571577 "noMessages": "Inga meddelanden hittades.",
572578 "sent": "skickad",
573573- "failed": "misslyckades"
579579+ "failed": "misslyckades",
580580+ "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.",
581581+ "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.",
582582+ "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto."
574583 },
575584 "repoExplorer": {
576585 "title": "Dataförvarsutforskare",
···689698 "orContinueWith": "Eller fortsätt med",
690699 "orUseCredentials": "Eller logga in med uppgifter"
691700 },
701701+ "register": {
702702+ "title": "Skapa konto",
703703+ "subtitle": "Skapa ett konto med {app}",
704704+ "subtitleGeneric": "Skapa ett konto för att fortsätta",
705705+ "haveAccount": "Har du redan ett konto?"
706706+ },
692707 "sso": {
693708 "linkedAccounts": "Länkade konton",
694709 "linkedAccountsDesc": "Externa konton länkade till din identitet för enkel inloggning.",
···822837 "error_expired": "Registreringssessionen har löpt ut. Försök igen.",
823838 "error_handle_required": "Välj ett användarnamn",
824839 "emailVerifiedByProvider": "Denna e-post är verifierad av {provider}. Ingen ytterligare verifiering behövs.",
825825- "emailChangedNeedsVerification": "Om du använder en annan e-post måste du verifiera den."
840840+ "emailChangedNeedsVerification": "Om du använder en annan e-post måste du verifiera den.",
841841+ "infoAfterTitle": "Efter att du skapat ditt konto",
842842+ "infoAddPassword": "Lägg till ett lösenord för traditionell inloggning",
843843+ "infoAddPasskey": "Konfigurera en nyckel för lösenordsfri inloggning",
844844+ "infoLinkProviders": "Länka ytterligare SSO-leverantörer",
845845+ "infoChangeHandle": "Byt användarnamn eller använd en egen domän",
846846+ "tryAgain": "Försök igen"
826847 },
827848 "verify": {
828849 "title": "Verifiera ditt konto",
···886907 "success": "Lösenord återställt!",
887908 "requestNewCode": "Begär ny kod",
888909 "passwordsMismatch": "Lösenorden matchar inte",
889889- "passwordLength": "Lösenordet måste vara minst 8 tecken"
910910+ "passwordLength": "Lösenordet måste vara minst 8 tecken",
911911+ "multipleAccountsWarning": "Flera konton delar denna e-post. Återställningskoden skickades till det senast skapade kontot. Använd ditt användarnamn istället för ett specifikt konto."
890912 },
891913 "recoverPasskey": {
892914 "title": "Återställ ditt konto",