···1237 "Should require lxm parameter for granular scopes"
1238 );
1239}
1240+1241+#[tokio::test]
1242+async fn test_oauth_metadata_includes_prompt_values_supported() {
1243+ let url = base_url().await;
1244+ let client = client();
1245+ let as_res = client
1246+ .get(format!("{}/.well-known/oauth-authorization-server", url))
1247+ .send()
1248+ .await
1249+ .unwrap();
1250+ assert_eq!(as_res.status(), StatusCode::OK);
1251+ let as_body: Value = as_res.json().await.unwrap();
1252+ let prompt_values = as_body["prompt_values_supported"]
1253+ .as_array()
1254+ .expect("prompt_values_supported should be an array");
1255+ assert!(
1256+ prompt_values.contains(&json!("none")),
1257+ "Should support prompt=none"
1258+ );
1259+ assert!(
1260+ prompt_values.contains(&json!("login")),
1261+ "Should support prompt=login"
1262+ );
1263+ assert!(
1264+ prompt_values.contains(&json!("consent")),
1265+ "Should support prompt=consent"
1266+ );
1267+ assert!(
1268+ prompt_values.contains(&json!("select_account")),
1269+ "Should support prompt=select_account"
1270+ );
1271+ assert!(
1272+ prompt_values.contains(&json!("create")),
1273+ "Should support prompt=create"
1274+ );
1275+}
1276+1277+#[tokio::test]
1278+async fn test_par_accepts_valid_prompt_values() {
1279+ let url = base_url().await;
1280+ let client = client();
1281+ let redirect_uri = "https://example.com/callback";
1282+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
1283+ let client_id = mock_client.uri();
1284+ let (_, code_challenge) = generate_pkce();
1285+ let valid_prompts = ["none", "login", "consent", "select_account", "create"];
1286+ for prompt in valid_prompts {
1287+ let par_res = client
1288+ .post(format!("{}/oauth/par", url))
1289+ .form(&[
1290+ ("response_type", "code"),
1291+ ("client_id", &client_id),
1292+ ("redirect_uri", redirect_uri),
1293+ ("code_challenge", &code_challenge),
1294+ ("code_challenge_method", "S256"),
1295+ ("scope", "atproto"),
1296+ ("state", "test-state"),
1297+ ("prompt", prompt),
1298+ ])
1299+ .send()
1300+ .await
1301+ .unwrap();
1302+ assert_eq!(
1303+ par_res.status(),
1304+ StatusCode::CREATED,
1305+ "PAR should accept prompt={}",
1306+ prompt
1307+ );
1308+ }
1309+}
1310+1311+#[tokio::test]
1312+async fn test_par_rejects_invalid_prompt_value() {
1313+ let url = base_url().await;
1314+ let client = client();
1315+ let redirect_uri = "https://example.com/callback";
1316+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
1317+ let client_id = mock_client.uri();
1318+ let (_, code_challenge) = generate_pkce();
1319+ let par_res = client
1320+ .post(format!("{}/oauth/par", url))
1321+ .form(&[
1322+ ("response_type", "code"),
1323+ ("client_id", &client_id),
1324+ ("redirect_uri", redirect_uri),
1325+ ("code_challenge", &code_challenge),
1326+ ("code_challenge_method", "S256"),
1327+ ("scope", "atproto"),
1328+ ("state", "test-state"),
1329+ ("prompt", "invalid_prompt"),
1330+ ])
1331+ .send()
1332+ .await
1333+ .unwrap();
1334+ assert_eq!(
1335+ par_res.status(),
1336+ StatusCode::BAD_REQUEST,
1337+ "PAR should reject invalid prompt value"
1338+ );
1339+ let body: Value = par_res.json().await.unwrap();
1340+ assert_eq!(body["error"], "invalid_request");
1341+ assert!(
1342+ body["error_description"]
1343+ .as_str()
1344+ .unwrap_or("")
1345+ .contains("prompt"),
1346+ "Error should mention prompt"
1347+ );
1348+}
1349+1350+#[tokio::test]
1351+async fn test_prompt_create_redirects_to_register() {
1352+ let url = base_url().await;
1353+ let client = no_redirect_client();
1354+ let redirect_uri = "https://example.com/callback";
1355+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
1356+ let client_id = mock_client.uri();
1357+ let (_, code_challenge) = generate_pkce();
1358+ let par_res = reqwest::Client::new()
1359+ .post(format!("{}/oauth/par", url))
1360+ .form(&[
1361+ ("response_type", "code"),
1362+ ("client_id", &client_id),
1363+ ("redirect_uri", redirect_uri),
1364+ ("code_challenge", &code_challenge),
1365+ ("code_challenge_method", "S256"),
1366+ ("scope", "atproto"),
1367+ ("state", "test-state"),
1368+ ("prompt", "create"),
1369+ ])
1370+ .send()
1371+ .await
1372+ .unwrap();
1373+ assert_eq!(par_res.status(), StatusCode::CREATED);
1374+ let par_body: Value = par_res.json().await.unwrap();
1375+ let request_uri = par_body["request_uri"].as_str().unwrap();
1376+ let auth_res = client
1377+ .get(format!("{}/oauth/authorize", url))
1378+ .query(&[("request_uri", request_uri)])
1379+ .send()
1380+ .await
1381+ .unwrap();
1382+ assert!(
1383+ auth_res.status().is_redirection(),
1384+ "Should redirect when prompt=create"
1385+ );
1386+ let location = auth_res
1387+ .headers()
1388+ .get("location")
1389+ .expect("Should have Location header")
1390+ .to_str()
1391+ .unwrap();
1392+ assert!(
1393+ location.contains("/app/oauth/register"),
1394+ "Should redirect to /app/oauth/register, got: {}",
1395+ location
1396+ );
1397+ assert!(
1398+ location.contains("request_uri="),
1399+ "Should include request_uri in redirect"
1400+ );
1401+}
1402+1403+#[tokio::test]
1404+async fn test_register_complete_rejects_invalid_request_uri() {
1405+ let url = base_url().await;
1406+ let client = client();
1407+ let res = client
1408+ .post(format!("{}/oauth/register/complete", url))
1409+ .json(&json!({
1410+ "request_uri": "urn:ietf:params:oauth:request_uri:nonexistent",
1411+ "did": "did:plc:test123",
1412+ "app_password": "test-password"
1413+ }))
1414+ .send()
1415+ .await
1416+ .unwrap();
1417+ assert_eq!(
1418+ res.status(),
1419+ StatusCode::BAD_REQUEST,
1420+ "Should reject invalid request_uri"
1421+ );
1422+ let body: Value = res.json().await.unwrap();
1423+ assert_eq!(body["error"], "invalid_request");
1424+}
1425+1426+#[tokio::test]
1427+async fn test_register_complete_rejects_wrong_credentials() {
1428+ let url = base_url().await;
1429+ let http_client = client();
1430+ let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
1431+ let handle = format!("rc{}", suffix);
1432+ let email = format!("rc{}@example.com", suffix);
1433+ let password = "Regcomplete123!";
1434+ let create_res = http_client
1435+ .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1436+ .json(&json!({ "handle": handle, "email": email, "password": password }))
1437+ .send()
1438+ .await
1439+ .unwrap();
1440+ assert_eq!(create_res.status(), StatusCode::OK);
1441+ let account: Value = create_res.json().await.unwrap();
1442+ let user_did = account["did"].as_str().unwrap();
1443+ verify_new_account(&http_client, user_did).await;
1444+ let redirect_uri = "https://example.com/callback";
1445+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
1446+ let client_id = mock_client.uri();
1447+ let (_, code_challenge) = generate_pkce();
1448+ let par_res = http_client
1449+ .post(format!("{}/oauth/par", url))
1450+ .form(&[
1451+ ("response_type", "code"),
1452+ ("client_id", &client_id),
1453+ ("redirect_uri", redirect_uri),
1454+ ("code_challenge", &code_challenge),
1455+ ("code_challenge_method", "S256"),
1456+ ("scope", "atproto"),
1457+ ("state", "test-state"),
1458+ ("prompt", "create"),
1459+ ])
1460+ .send()
1461+ .await
1462+ .unwrap();
1463+ let par_body: Value = par_res.json().await.unwrap();
1464+ let request_uri = par_body["request_uri"].as_str().unwrap();
1465+ let res = http_client
1466+ .post(format!("{}/oauth/register/complete", url))
1467+ .json(&json!({
1468+ "request_uri": request_uri,
1469+ "did": user_did,
1470+ "app_password": "wrong-password"
1471+ }))
1472+ .send()
1473+ .await
1474+ .unwrap();
1475+ assert_eq!(
1476+ res.status(),
1477+ StatusCode::FORBIDDEN,
1478+ "Should reject wrong credentials"
1479+ );
1480+ let body: Value = res.json().await.unwrap();
1481+ assert_eq!(body["error"], "access_denied");
1482+}
1483+1484+#[tokio::test]
1485+async fn test_full_oauth_registration_flow() {
1486+ let url = base_url().await;
1487+ let http_client = client();
1488+1489+ let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
1490+ let handle = format!("oauthreg{}", suffix);
1491+ let email = format!("oauthreg{}@example.com", suffix);
1492+ let password = "OauthRegTest123!";
1493+1494+ let redirect_uri = "https://example.com/callback";
1495+ let mock_client = setup_mock_client_metadata(redirect_uri).await;
1496+ let client_id = mock_client.uri();
1497+ let (code_verifier, code_challenge) = generate_pkce();
1498+ let state = format!("state-{}", suffix);
1499+1500+ let par_res = http_client
1501+ .post(format!("{}/oauth/par", url))
1502+ .form(&[
1503+ ("response_type", "code"),
1504+ ("client_id", &client_id),
1505+ ("redirect_uri", redirect_uri),
1506+ ("code_challenge", &code_challenge),
1507+ ("code_challenge_method", "S256"),
1508+ ("scope", "atproto"),
1509+ ("state", &state),
1510+ ("prompt", "create"),
1511+ ])
1512+ .send()
1513+ .await
1514+ .unwrap();
1515+ assert_eq!(
1516+ par_res.status(),
1517+ StatusCode::CREATED,
1518+ "PAR with prompt=create should succeed"
1519+ );
1520+ let par_body: Value = par_res.json().await.unwrap();
1521+ let request_uri = par_body["request_uri"].as_str().unwrap();
1522+1523+ let create_res = http_client
1524+ .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
1525+ .json(&json!({ "handle": handle, "email": email, "password": password }))
1526+ .send()
1527+ .await
1528+ .unwrap();
1529+ assert_eq!(
1530+ create_res.status(),
1531+ StatusCode::OK,
1532+ "Account creation should succeed"
1533+ );
1534+ let account: Value = create_res.json().await.unwrap();
1535+ let user_did = account["did"].as_str().unwrap();
1536+ let access_jwt = account["accessJwt"].as_str().unwrap();
1537+1538+ let app_password_res = http_client
1539+ .post(format!(
1540+ "{}/xrpc/com.atproto.server.createAppPassword",
1541+ url
1542+ ))
1543+ .header("Authorization", format!("Bearer {}", access_jwt))
1544+ .json(&json!({ "name": "oauth-test-app" }))
1545+ .send()
1546+ .await
1547+ .unwrap();
1548+ assert_eq!(
1549+ app_password_res.status(),
1550+ StatusCode::OK,
1551+ "App password creation should succeed"
1552+ );
1553+ let app_password_body: Value = app_password_res.json().await.unwrap();
1554+ let app_password = app_password_body["password"].as_str().unwrap();
1555+1556+ verify_new_account(&http_client, user_did).await;
1557+1558+ let complete_res = http_client
1559+ .post(format!("{}/oauth/register/complete", url))
1560+ .json(&json!({
1561+ "request_uri": request_uri,
1562+ "did": user_did,
1563+ "app_password": app_password
1564+ }))
1565+ .send()
1566+ .await
1567+ .unwrap();
1568+ assert_eq!(
1569+ complete_res.status(),
1570+ StatusCode::OK,
1571+ "register_complete should succeed"
1572+ );
1573+ let complete_body: Value = complete_res.json().await.unwrap();
1574+ let mut redirect_location = complete_body["redirect_uri"]
1575+ .as_str()
1576+ .expect("Expected redirect_uri from register_complete")
1577+ .to_string();
1578+1579+ if redirect_location.contains("/oauth/consent") {
1580+ let consent_res = http_client
1581+ .post(format!("{}/oauth/authorize/consent", url))
1582+ .header("Content-Type", "application/json")
1583+ .json(&json!({
1584+ "request_uri": request_uri,
1585+ "approved_scopes": ["atproto"],
1586+ "remember": false
1587+ }))
1588+ .send()
1589+ .await
1590+ .unwrap();
1591+ assert_eq!(
1592+ consent_res.status(),
1593+ StatusCode::OK,
1594+ "Consent should succeed"
1595+ );
1596+ let consent_body: Value = consent_res.json().await.unwrap();
1597+ redirect_location = consent_body["redirect_uri"]
1598+ .as_str()
1599+ .expect("Expected redirect_uri from consent")
1600+ .to_string();
1601+ }
1602+1603+ assert!(
1604+ redirect_location.contains("code="),
1605+ "Should have authorization code in redirect: {}",
1606+ redirect_location
1607+ );
1608+1609+ let code = redirect_location
1610+ .split("code=")
1611+ .nth(1)
1612+ .unwrap()
1613+ .split('&')
1614+ .next()
1615+ .unwrap();
1616+1617+ let token_res = http_client
1618+ .post(format!("{}/oauth/token", url))
1619+ .form(&[
1620+ ("grant_type", "authorization_code"),
1621+ ("code", code),
1622+ ("redirect_uri", redirect_uri),
1623+ ("code_verifier", &code_verifier),
1624+ ("client_id", &client_id),
1625+ ])
1626+ .send()
1627+ .await
1628+ .unwrap();
1629+ assert_eq!(
1630+ token_res.status(),
1631+ StatusCode::OK,
1632+ "Token exchange should succeed"
1633+ );
1634+ let token_body: Value = token_res.json().await.unwrap();
1635+ assert!(
1636+ token_body["access_token"].is_string(),
1637+ "Should have access_token"
1638+ );
1639+ assert!(
1640+ token_body["refresh_token"].is_string(),
1641+ "Should have refresh_token"
1642+ );
1643+ assert_eq!(token_body["token_type"], "Bearer");
1644+ assert_eq!(
1645+ token_body["sub"], user_did,
1646+ "Token sub should match user DID"
1647+ );
1648+}
+1-1
crates/tranquil-pds/tests/sso.rs
···10391040 let redirect_url = body["redirectUrl"].as_str().unwrap();
1041 assert!(
1042- redirect_url.contains("/app/oauth/verify"),
1043 "Non-auto-verified channel should redirect to verify, got: {}",
1044 redirect_url
1045 );
···10391040 let redirect_url = body["redirectUrl"].as_str().unwrap();
1041 assert!(
1042+ redirect_url.contains("/app/verify"),
1043 "Non-auto-verified channel should redirect to verify, got: {}",
1044 redirect_url
1045 );
+3-3
frontend/public/homepage.html
···461 <div class="feature">
462 <h3>Real security</h3>
463 <p>
464- Sign in with passkeys or SSO, add two-factor authentication,
465- set up backup codes, and mark devices you trust. Your account
466- stays yours.
467 </p>
468 </div>
469
···461 <div class="feature">
462 <h3>Real security</h3>
463 <p>
464+ Sign in with passkeys or SSO, add two-factor authentication, set
465+ up backup codes, and mark devices you trust. Your account stays
466+ yours.
467 </p>
468 </div>
469
+12-10
frontend/src/App.svelte
···6 import { isLoading as i18nLoading } from 'svelte-i18n'
7 import Toast from './components/Toast.svelte'
8 import Login from './routes/Login.svelte'
9- import Register from './routes/Register.svelte'
10- import RegisterPasskey from './routes/RegisterPasskey.svelte'
11 import RegisterSso from './routes/RegisterSso.svelte'
12 import Verify from './routes/Verify.svelte'
13 import ResetPassword from './routes/ResetPassword.svelte'
···29 import OAuthPasskey from './routes/OAuthPasskey.svelte'
30 import OAuthDelegation from './routes/OAuthDelegation.svelte'
31 import OAuthError from './routes/OAuthError.svelte'
32- import OAuthSsoRegister from './routes/OAuthSsoRegister.svelte'
0033 import Security from './routes/Security.svelte'
34 import TrustedDevices from './routes/TrustedDevices.svelte'
35 import Controllers from './routes/Controllers.svelte'
···98 switch (path) {
99 case '/login':
100 return Login
101- case '/register':
102- return RegisterPasskey
103- case '/register-password':
104- return Register
105- case '/register-sso':
106- return RegisterSso
107 case '/verify':
108 return Verify
109 case '/reset-password':
···145 case '/oauth/error':
146 return OAuthError
147 case '/oauth/sso-register':
148- return OAuthSsoRegister
0000000149 case '/security':
150 return Security
151 case '/trusted-devices':
···167168 let currentPath = $derived(getCurrentPath())
169 let CurrentComponent = $derived(getComponent(currentPath))
0170</script>
171172<main>
···6 import { isLoading as i18nLoading } from 'svelte-i18n'
7 import Toast from './components/Toast.svelte'
8 import Login from './routes/Login.svelte'
009 import RegisterSso from './routes/RegisterSso.svelte'
10 import Verify from './routes/Verify.svelte'
11 import ResetPassword from './routes/ResetPassword.svelte'
···27 import OAuthPasskey from './routes/OAuthPasskey.svelte'
28 import OAuthDelegation from './routes/OAuthDelegation.svelte'
29 import OAuthError from './routes/OAuthError.svelte'
30+ import SsoRegisterComplete from './routes/SsoRegisterComplete.svelte'
31+ import Register from './routes/Register.svelte'
32+ import RegisterPassword from './routes/RegisterPassword.svelte'
33 import Security from './routes/Security.svelte'
34 import TrustedDevices from './routes/TrustedDevices.svelte'
35 import Controllers from './routes/Controllers.svelte'
···98 switch (path) {
99 case '/login':
100 return Login
000000101 case '/verify':
102 return Verify
103 case '/reset-password':
···139 case '/oauth/error':
140 return OAuthError
141 case '/oauth/sso-register':
142+ return SsoRegisterComplete
143+ case '/register':
144+ case '/oauth/register':
145+ return Register
146+ case '/oauth/register-sso':
147+ return RegisterSso
148+ case '/oauth/register-password':
149+ return RegisterPassword
150 case '/security':
151 return Security
152 case '/trusted-devices':
···168169 let currentPath = $derived(getCurrentPath())
170 let CurrentComponent = $derived(getComponent(currentPath))
171+172</script>
173174<main>
···1-export * from "./types";
2-export * from "./flow.svelte";
3export { default as VerificationStep } from "./VerificationStep.svelte";
4export { default as KeyChoiceStep } from "./KeyChoiceStep.svelte";
5export { default as DidDocStep } from "./DidDocStep.svelte";
···1+export * from "./types.ts";
2+export * from "./flow.svelte.ts";
3export { default as VerificationStep } from "./VerificationStep.svelte";
4export { default as KeyChoiceStep } from "./KeyChoiceStep.svelte";
5export { default as DidDocStep } from "./DidDocStep.svelte";
+1-1
frontend/src/lib/serverConfig.svelte.ts
···1-import { api } from "./api";
23interface ServerConfigState {
4 serverName: string | null;
···1+import { api } from "./api.ts";
23interface ServerConfigState {
4 serverName: string | null;
···150 "email": "Email",
151 "emailAddress": "Email Address",
152 "emailPlaceholder": "you@example.com",
0153 "discord": "Discord",
154 "discordId": "Discord User ID",
155 "discordIdPlaceholder": "Your Discord user ID",
156 "discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)",
0157 "telegram": "Telegram",
158 "telegramUsername": "Telegram Username",
159 "telegramUsernamePlaceholder": "@yourusername",
0160 "signal": "Signal",
161 "signalNumber": "Signal Phone Number",
162 "signalNumberPlaceholder": "+1234567890",
163 "signalNumberHint": "Include country code (eg., +1 for US)",
0164 "notConfigured": "not configured",
165 "inviteCode": "Invite Code",
166 "inviteCodePlaceholder": "Enter your invite code",
···275 "currentEmail": "Current: {email}",
276 "newEmail": "New Email",
277 "newEmailPlaceholder": "new@example.com",
0278 "changeEmailButton": "Change Email",
279 "requesting": "Requesting...",
280 "verificationCode": "Verification Code",
···437 "noCodes": "No invite codes yet",
438 "available": "Available",
439 "used": "Used by @{handle}",
0440 "disabled": "Disabled",
441 "usedBy": "Used by",
442 "disableConfirm": "Disable this invite code? It can no longer be used.",
···577 "hideHistory": "Hide History",
578 "noMessages": "No messages found.",
579 "sent": "sent",
580- "failed": "failed"
000581 },
582 "repoExplorer": {
583 "title": "Repository Explorer",
···777 "subtitle": "Select an account to continue",
778 "useAnother": "Use a different account"
779 },
000000780 "twoFactor": {
781 "title": "Two-Factor Authentication",
782 "subtitle": "Additional verification is required",
···887 "sendCode": "Send Reset Code",
888 "sending": "Sending...",
889 "codeSent": "Password reset code sent! Check your preferred notification channel.",
0890 "enterCode": "Enter the code you received and your new password.",
891 "code": "Reset Code",
892 "codePlaceholder": "Enter reset code",
···150 "email": "Email",
151 "emailAddress": "Email Address",
152 "emailPlaceholder": "you@example.com",
153+ "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.",
154 "discord": "Discord",
155 "discordId": "Discord User ID",
156 "discordIdPlaceholder": "Your Discord user ID",
157 "discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)",
158+ "discordInUseWarning": "This Discord ID is already associated with another account.",
159 "telegram": "Telegram",
160 "telegramUsername": "Telegram Username",
161 "telegramUsernamePlaceholder": "@yourusername",
162+ "telegramInUseWarning": "This Telegram username is already associated with another account.",
163 "signal": "Signal",
164 "signalNumber": "Signal Phone Number",
165 "signalNumberPlaceholder": "+1234567890",
166 "signalNumberHint": "Include country code (eg., +1 for US)",
167+ "signalInUseWarning": "This Signal number is already associated with another account.",
168 "notConfigured": "not configured",
169 "inviteCode": "Invite Code",
170 "inviteCodePlaceholder": "Enter your invite code",
···279 "currentEmail": "Current: {email}",
280 "newEmail": "New Email",
281 "newEmailPlaceholder": "new@example.com",
282+ "emailInUseWarning": "This email is already used by another account. You can still use it, but account recovery may require your handle.",
283 "changeEmailButton": "Change Email",
284 "requesting": "Requesting...",
285 "verificationCode": "Verification Code",
···442 "noCodes": "No invite codes yet",
443 "available": "Available",
444 "used": "Used by @{handle}",
445+ "spent": "Spent",
446 "disabled": "Disabled",
447 "usedBy": "Used by",
448 "disableConfirm": "Disable this invite code? It can no longer be used.",
···583 "hideHistory": "Hide History",
584 "noMessages": "No messages found.",
585 "sent": "sent",
586+ "failed": "failed",
587+ "discordInUseWarning": "This Discord ID is already associated with another account.",
588+ "telegramInUseWarning": "This Telegram username is already associated with another account.",
589+ "signalInUseWarning": "This Signal number is already associated with another account."
590 },
591 "repoExplorer": {
592 "title": "Repository Explorer",
···786 "subtitle": "Select an account to continue",
787 "useAnother": "Use a different account"
788 },
789+ "register": {
790+ "title": "Create Account",
791+ "subtitle": "Create an account to continue to",
792+ "subtitleGeneric": "Create an account to continue",
793+ "haveAccount": "Already have an account? Sign in"
794+ },
795 "twoFactor": {
796 "title": "Two-Factor Authentication",
797 "subtitle": "Additional verification is required",
···902 "sendCode": "Send Reset Code",
903 "sending": "Sending...",
904 "codeSent": "Password reset code sent! Check your preferred notification channel.",
905+ "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.",
906 "enterCode": "Enter the code you received and your new password.",
907 "code": "Reset Code",
908 "codePlaceholder": "Enter reset code",
···143 "email": "이메일",
144 "emailAddress": "이메일 주소",
145 "emailPlaceholder": "you@example.com",
0146 "discord": "Discord",
147 "discordId": "Discord 사용자 ID",
148 "discordIdPlaceholder": "Discord 사용자 ID",
149 "discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
0150 "telegram": "Telegram",
151 "telegramUsername": "Telegram 사용자 이름",
152 "telegramUsernamePlaceholder": "@yourusername",
0153 "signal": "Signal",
154 "signalNumber": "Signal 전화번호",
155 "signalNumberPlaceholder": "+821012345678",
156 "signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
0157 "notConfigured": "구성되지 않음",
158 "inviteCode": "초대 코드",
159 "inviteCodePlaceholder": "초대 코드 입력",
···269 "newEmail": "새 이메일",
270 "newEmailPlaceholder": "new@example.com",
271 "changeEmailButton": "이메일 변경",
0272 "requesting": "요청 중...",
273 "verificationCode": "인증 코드",
274 "verificationCodePlaceholder": "인증 코드 입력",
···430 "noCodes": "초대 코드가 아직 없습니다",
431 "available": "사용 가능",
432 "used": "@{handle}이(가) 사용함",
0433 "disabled": "비활성화됨",
434 "usedBy": "사용자",
435 "disableConfirm": "이 초대 코드를 비활성화하시겠습니까? 더 이상 사용할 수 없습니다.",
···570 "hideHistory": "기록 숨기기",
571 "noMessages": "메시지가 없습니다.",
572 "sent": "전송됨",
573- "failed": "실패"
000574 },
575 "repoExplorer": {
576 "title": "저장소 탐색기",
···689 "orContinueWith": "또는 다음으로 계속",
690 "orUseCredentials": "또는 자격 증명으로 로그인"
691 },
000000692 "sso": {
693 "linkedAccounts": "연결된 계정",
694 "linkedAccountsDesc": "싱글 사인온을 위해 연결된 외부 계정입니다.",
···822 "error_expired": "등록 세션이 만료되었습니다. 다시 시도해 주세요.",
823 "error_handle_required": "핸들을 선택해 주세요",
824 "emailVerifiedByProvider": "이 이메일은 {provider}에서 인증되었습니다. 추가 인증이 필요하지 않습니다.",
825- "emailChangedNeedsVerification": "다른 이메일을 사용하시면 인증이 필요합니다."
000000826 },
827 "verify": {
828 "title": "계정 인증",
···886 "success": "비밀번호가 재설정되었습니다!",
887 "requestNewCode": "새 코드 요청",
888 "passwordsMismatch": "비밀번호가 일치하지 않습니다",
889- "passwordLength": "비밀번호는 8자 이상이어야 합니다"
0890 },
891 "recoverPasskey": {
892 "title": "계정 복구",
···143 "email": "이메일",
144 "emailAddress": "이메일 주소",
145 "emailPlaceholder": "you@example.com",
146+ "emailInUseWarning": "이 이메일은 이미 다른 계정과 연결되어 있습니다. 계속 사용할 수 있지만, 계정 복구 시 핸들이 필요할 수 있습니다.",
147 "discord": "Discord",
148 "discordId": "Discord 사용자 ID",
149 "discordIdPlaceholder": "Discord 사용자 ID",
150 "discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)",
151+ "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.",
152 "telegram": "Telegram",
153 "telegramUsername": "Telegram 사용자 이름",
154 "telegramUsernamePlaceholder": "@yourusername",
155+ "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.",
156 "signal": "Signal",
157 "signalNumber": "Signal 전화번호",
158 "signalNumberPlaceholder": "+821012345678",
159 "signalNumberHint": "국가 코드 포함 (예: 한국 +82)",
160+ "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다.",
161 "notConfigured": "구성되지 않음",
162 "inviteCode": "초대 코드",
163 "inviteCodePlaceholder": "초대 코드 입력",
···273 "newEmail": "새 이메일",
274 "newEmailPlaceholder": "new@example.com",
275 "changeEmailButton": "이메일 변경",
276+ "emailInUseWarning": "이 이메일은 이미 다른 계정과 연결되어 있습니다. 계속 사용하실 수 있지만, 계정 복구 시 이메일 대신 핸들을 사용해야 할 수 있습니다.",
277 "requesting": "요청 중...",
278 "verificationCode": "인증 코드",
279 "verificationCodePlaceholder": "인증 코드 입력",
···435 "noCodes": "초대 코드가 아직 없습니다",
436 "available": "사용 가능",
437 "used": "@{handle}이(가) 사용함",
438+ "spent": "소진됨",
439 "disabled": "비활성화됨",
440 "usedBy": "사용자",
441 "disableConfirm": "이 초대 코드를 비활성화하시겠습니까? 더 이상 사용할 수 없습니다.",
···576 "hideHistory": "기록 숨기기",
577 "noMessages": "메시지가 없습니다.",
578 "sent": "전송됨",
579+ "failed": "실패",
580+ "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.",
581+ "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.",
582+ "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다."
583 },
584 "repoExplorer": {
585 "title": "저장소 탐색기",
···698 "orContinueWith": "또는 다음으로 계속",
699 "orUseCredentials": "또는 자격 증명으로 로그인"
700 },
701+ "register": {
702+ "title": "계정 만들기",
703+ "subtitle": "계속하려면 계정을 만드세요",
704+ "subtitleGeneric": "계속하려면 계정을 만드세요",
705+ "haveAccount": "이미 계정이 있으신가요? 로그인"
706+ },
707 "sso": {
708 "linkedAccounts": "연결된 계정",
709 "linkedAccountsDesc": "싱글 사인온을 위해 연결된 외부 계정입니다.",
···837 "error_expired": "등록 세션이 만료되었습니다. 다시 시도해 주세요.",
838 "error_handle_required": "핸들을 선택해 주세요",
839 "emailVerifiedByProvider": "이 이메일은 {provider}에서 인증되었습니다. 추가 인증이 필요하지 않습니다.",
840+ "emailChangedNeedsVerification": "다른 이메일을 사용하시면 인증이 필요합니다.",
841+ "infoAfterTitle": "계정 생성 후",
842+ "infoAddPassword": "기존 로그인을 위한 비밀번호 추가",
843+ "infoAddPasskey": "비밀번호 없는 로그인을 위한 패스키 설정",
844+ "infoLinkProviders": "추가 SSO 제공자 연결",
845+ "infoChangeHandle": "핸들 변경 또는 사용자 정의 도메인 사용",
846+ "tryAgain": "다시 시도"
847 },
848 "verify": {
849 "title": "계정 인증",
···907 "success": "비밀번호가 재설정되었습니다!",
908 "requestNewCode": "새 코드 요청",
909 "passwordsMismatch": "비밀번호가 일치하지 않습니다",
910+ "passwordLength": "비밀번호는 8자 이상이어야 합니다",
911+ "multipleAccountsWarning": "여러 계정에서 이 이메일을 공유하고 있습니다. 재설정 코드는 가장 최근에 생성된 계정으로 전송되었습니다. 특정 계정을 복구하려면 핸들을 사용하세요."
912 },
913 "recoverPasskey": {
914 "title": "계정 복구",
+26-4
frontend/src/locales/sv.json
···147 "discordId": "Discord användar-ID",
148 "discordIdPlaceholder": "Ditt Discord användar-ID",
149 "discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
0150 "telegram": "Telegram",
151 "telegramUsername": "Telegram-användarnamn",
152 "telegramUsernamePlaceholder": "@dittanvändarnamn",
0153 "signal": "Signal",
154 "signalNumber": "Signal-telefonnummer",
155 "signalNumberPlaceholder": "+46701234567",
156 "signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
0157 "notConfigured": "ej konfigurerad",
158 "inviteCode": "Inbjudningskod",
159 "inviteCodePlaceholder": "Ange din inbjudningskod",
···161 "createButton": "Skapa konto",
162 "alreadyHaveAccount": "Har du redan ett konto?",
163 "signIn": "Logga in",
0164 "passkeyAccount": "Nyckel",
165 "passwordAccount": "Lösenord",
166 "ssoAccount": "SSO",
···269 "newEmail": "Ny e-post",
270 "newEmailPlaceholder": "ny@exempel.se",
271 "changeEmailButton": "Ändra e-post",
0272 "requesting": "Begär...",
273 "verificationCode": "Verifieringskod",
274 "verificationCodePlaceholder": "Ange verifieringskod",
···435 "disableConfirm": "Inaktivera denna inbjudningskod? Den kan inte längre användas.",
436 "created": "Inbjudningskod skapad",
437 "copy": "Kopiera",
438- "createdOn": "Skapad {date}"
0439 },
440 "security": {
441 "title": "Säkerhet",
···570 "hideHistory": "Dölj historik",
571 "noMessages": "Inga meddelanden hittades.",
572 "sent": "skickad",
573- "failed": "misslyckades"
000574 },
575 "repoExplorer": {
576 "title": "Dataförvarsutforskare",
···689 "orContinueWith": "Eller fortsätt med",
690 "orUseCredentials": "Eller logga in med uppgifter"
691 },
000000692 "sso": {
693 "linkedAccounts": "Länkade konton",
694 "linkedAccountsDesc": "Externa konton länkade till din identitet för enkel inloggning.",
···822 "error_expired": "Registreringssessionen har löpt ut. Försök igen.",
823 "error_handle_required": "Välj ett användarnamn",
824 "emailVerifiedByProvider": "Denna e-post är verifierad av {provider}. Ingen ytterligare verifiering behövs.",
825- "emailChangedNeedsVerification": "Om du använder en annan e-post måste du verifiera den."
000000826 },
827 "verify": {
828 "title": "Verifiera ditt konto",
···886 "success": "Lösenord återställt!",
887 "requestNewCode": "Begär ny kod",
888 "passwordsMismatch": "Lösenorden matchar inte",
889- "passwordLength": "Lösenordet måste vara minst 8 tecken"
0890 },
891 "recoverPasskey": {
892 "title": "Återställ ditt konto",
···147 "discordId": "Discord användar-ID",
148 "discordIdPlaceholder": "Ditt Discord användar-ID",
149 "discordIdHint": "Ditt numeriska Discord användar-ID (aktivera Utvecklarläge för att hitta det)",
150+ "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.",
151 "telegram": "Telegram",
152 "telegramUsername": "Telegram-användarnamn",
153 "telegramUsernamePlaceholder": "@dittanvändarnamn",
154+ "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.",
155 "signal": "Signal",
156 "signalNumber": "Signal-telefonnummer",
157 "signalNumberPlaceholder": "+46701234567",
158 "signalNumberHint": "Inkludera landskod (t.ex. +46 för Sverige)",
159+ "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto.",
160 "notConfigured": "ej konfigurerad",
161 "inviteCode": "Inbjudningskod",
162 "inviteCodePlaceholder": "Ange din inbjudningskod",
···164 "createButton": "Skapa konto",
165 "alreadyHaveAccount": "Har du redan ett konto?",
166 "signIn": "Logga in",
167+ "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.",
168 "passkeyAccount": "Nyckel",
169 "passwordAccount": "Lösenord",
170 "ssoAccount": "SSO",
···273 "newEmail": "Ny e-post",
274 "newEmailPlaceholder": "ny@exempel.se",
275 "changeEmailButton": "Ändra e-post",
276+ "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.",
277 "requesting": "Begär...",
278 "verificationCode": "Verifieringskod",
279 "verificationCodePlaceholder": "Ange verifieringskod",
···440 "disableConfirm": "Inaktivera denna inbjudningskod? Den kan inte längre användas.",
441 "created": "Inbjudningskod skapad",
442 "copy": "Kopiera",
443+ "createdOn": "Skapad {date}",
444+ "spent": "Förbrukad"
445 },
446 "security": {
447 "title": "Säkerhet",
···576 "hideHistory": "Dölj historik",
577 "noMessages": "Inga meddelanden hittades.",
578 "sent": "skickad",
579+ "failed": "misslyckades",
580+ "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.",
581+ "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.",
582+ "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto."
583 },
584 "repoExplorer": {
585 "title": "Dataförvarsutforskare",
···698 "orContinueWith": "Eller fortsätt med",
699 "orUseCredentials": "Eller logga in med uppgifter"
700 },
701+ "register": {
702+ "title": "Skapa konto",
703+ "subtitle": "Skapa ett konto med {app}",
704+ "subtitleGeneric": "Skapa ett konto för att fortsätta",
705+ "haveAccount": "Har du redan ett konto?"
706+ },
707 "sso": {
708 "linkedAccounts": "Länkade konton",
709 "linkedAccountsDesc": "Externa konton länkade till din identitet för enkel inloggning.",
···837 "error_expired": "Registreringssessionen har löpt ut. Försök igen.",
838 "error_handle_required": "Välj ett användarnamn",
839 "emailVerifiedByProvider": "Denna e-post är verifierad av {provider}. Ingen ytterligare verifiering behövs.",
840+ "emailChangedNeedsVerification": "Om du använder en annan e-post måste du verifiera den.",
841+ "infoAfterTitle": "Efter att du skapat ditt konto",
842+ "infoAddPassword": "Lägg till ett lösenord för traditionell inloggning",
843+ "infoAddPasskey": "Konfigurera en nyckel för lösenordsfri inloggning",
844+ "infoLinkProviders": "Länka ytterligare SSO-leverantörer",
845+ "infoChangeHandle": "Byt användarnamn eller använd en egen domän",
846+ "tryAgain": "Försök igen"
847 },
848 "verify": {
849 "title": "Verifiera ditt konto",
···907 "success": "Lösenord återställt!",
908 "requestNewCode": "Begär ny kod",
909 "passwordsMismatch": "Lösenorden matchar inte",
910+ "passwordLength": "Lösenordet måste vara minst 8 tecken",
911+ "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."
912 },
913 "recoverPasskey": {
914 "title": "Återställ ditt konto",