mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-04 09:16:54 +00:00
fix: user handle domains upgrade
This commit is contained in:
@@ -21,6 +21,10 @@ heavy-load-tests = { max-threads = 4 }
|
||||
filter = "test(/import_with_verification/) | test(/plc_migration/)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(handle_domains)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(ripple_cluster)"
|
||||
test-group = "serial-env-tests"
|
||||
@@ -41,6 +45,10 @@ test-group = "heavy-load-tests"
|
||||
filter = "test(/import_with_verification/) | test(/plc_migration/)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(handle_domains)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(ripple_cluster)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
Generated
+15
-15
@@ -6094,7 +6094,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-auth"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base32",
|
||||
@@ -6117,7 +6117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-cache"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6131,7 +6131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-comms"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6146,7 +6146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-config"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"confique",
|
||||
"serde",
|
||||
@@ -6154,7 +6154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-crypto"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
@@ -6170,7 +6170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -6187,7 +6187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db-traits"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6203,7 +6203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-infra"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -6214,7 +6214,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -6237,7 +6237,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-pds"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -6324,7 +6324,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-repo"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cid",
|
||||
@@ -6336,7 +6336,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-ripple"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
@@ -6361,7 +6361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-scopes"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
@@ -6377,7 +6377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-storage"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
@@ -6394,7 +6394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-types"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cid",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# Lewis' Big Boy TODO list
|
||||
|
||||
## Active development
|
||||
|
||||
### Storage backend abstraction
|
||||
Make storage layers swappable via traits.
|
||||
|
||||
sqlite database backend
|
||||
- [ ] abstract db layer behind trait (queries, transactions, migrations)
|
||||
- [ ] sqlite implementation matching postgres behavior
|
||||
- [ ] handle sqlite's single-writer limitation (connection pooling strategy)
|
||||
- [ ] migrations system that works for both
|
||||
- [ ] testing: run full test suite against both backends
|
||||
- [ ] config option to choose backend (postgres vs sqlite)
|
||||
- [ ] document tradeoffs (sqlite for single-user/small, postgres for multi-user/scale)
|
||||
|
||||
- [ ] skip sqlite and just straight-up do our own db?!
|
||||
|
||||
### Plugin system
|
||||
WASM component model plugins. Compile to wasm32-wasip2, sandboxed via wasmtime, capability-gated. Based on zed's extensions.
|
||||
|
||||
WIT interface
|
||||
- [ ] record hooks before/after create, update, delete
|
||||
- [ ] blob hooks before/after upload, validate
|
||||
- [ ] xrpc hooks before/after (middleware), custom endpoint handler
|
||||
- [ ] firehose hook on_commit
|
||||
- [ ] host imports http client, kv store, logging, read records
|
||||
|
||||
wasmtime host
|
||||
- [ ] engine with epoch interruption (kill runaway plugins)
|
||||
- [ ] plugin manifest (plugin.toml): id, version, capabilities, hooks
|
||||
- [ ] capability enforcement at runtime
|
||||
- [ ] plugin loader, lifecycle (enable/disable/reload)
|
||||
- [ ] resource limits (memory, time)
|
||||
- [ ] per-plugin fs sandbox
|
||||
|
||||
capabilities
|
||||
- [ ] http:fetch with domain allowlist
|
||||
- [ ] kv:read, kv:write
|
||||
- [ ] record:read, blob:read
|
||||
- [ ] xrpc:register
|
||||
- [ ] firehose:subscribe
|
||||
|
||||
pds-plugin-api (rust), MVP for plugin system
|
||||
- [ ] plugin trait with default impls
|
||||
- [ ] register_plugin! macro
|
||||
- [ ] typed host import wrappers
|
||||
- [ ] publish to crates.io
|
||||
- [ ] docs + example
|
||||
|
||||
pds-plugin-api in golang, nice to have after the fact
|
||||
- [ ] wit-bindgen-go bindings
|
||||
- [ ] go wrappers
|
||||
- [ ] tinygo build instructions
|
||||
- [ ] example
|
||||
|
||||
@pds/plugin-api in typescript, nice to have after the fact
|
||||
- [ ] jco/componentize-js bindings
|
||||
- [ ] typeScript types
|
||||
- [ ] build tooling
|
||||
- [ ] example
|
||||
|
||||
example plugins
|
||||
- [ ] content filter
|
||||
- [ ] webhook notifier
|
||||
- [ ] objsto backup mirror
|
||||
- [ ] custom lexicon handler
|
||||
- [ ] better audit logger
|
||||
|
||||
### Misc
|
||||
|
||||
cross-pds delegation
|
||||
when a client (eg. tangled.org) tries to log into a delegated account:
|
||||
- [ ] client starts oauth flow to delegated account's pds
|
||||
- [ ] delegated pds sees account is externally controlled, launches oauth to controller's pds (delegated pds acts as oauth client)
|
||||
- [ ] controller authenticates at their own pds
|
||||
- [ ] delegated pds verifies controller perms and scope from its local delegation grants
|
||||
- [ ] delegated pds issues session to client within the intersection of controller's granted scope and client's requested scope
|
||||
|
||||
per-request "act as"
|
||||
- [ ] authed as user X, perform action as delegated user Y in single request
|
||||
- [ ] approach decision
|
||||
- [ ] option 1: `X-Act-As` header with target did, server verifies delegation grant
|
||||
- [ ] option 2: token exchange (RFC 8693) for short-lived delegated token
|
||||
- [ ] option 3 (lewis fav): extend existing `act` claim to support on-demand minting
|
||||
- [ ] something else?
|
||||
|
||||
### Private/encrypted data
|
||||
Records only authorized parties can see and decrypt.
|
||||
|
||||
research
|
||||
- [ ] survey atproto discourse on private data
|
||||
- [ ] document bluesky team's likely approach. wait.. are they even gonna do this? whatever
|
||||
- [ ] look at matrix/signal for federated e2ee patterns
|
||||
|
||||
key management
|
||||
- [ ] db schema for encryption keys (user_keys, key_grants, key_rotations)
|
||||
- [ ] per-user encryption keypair generation (separate from signing keys)
|
||||
- [ ] key derivation scheme (per-collection? per-record? both?)
|
||||
- [ ] key storage (encrypted at rest, hsm option?)
|
||||
- [ ] rotation and revocation flow
|
||||
|
||||
storage layer
|
||||
- [ ] encrypted record format (encrypted cbor blob + metadata)
|
||||
- [ ] collection-level vs per-record encryption flag
|
||||
- [ ] how encrypted records appear in mst (hash of ciphertext? separate tree?)
|
||||
- [ ] blob encryption (same keys? separate?)
|
||||
|
||||
api surface
|
||||
- [ ] xrpc getPublicKey, grantAccess, revokeAccess, listGrants
|
||||
- [ ] xrpc getEncryptedRecord (ciphertext for client-side decrypt)
|
||||
- [ ] or transparent server-side decrypt if requester has grant?
|
||||
- [ ] lexicon for key grant records
|
||||
|
||||
sync/federation
|
||||
- [ ] how encrypted records appear on firehose (ciphertext? omitted? placeholder?)
|
||||
- [ ] pds-to-pds key exchange protocol
|
||||
- [ ] appview behavior (can't index without grants)
|
||||
- [ ] relay behavior with encrypted commits
|
||||
|
||||
client integration
|
||||
- [ ] client-side encryption (pds never sees plaintext) vs server-side with trust
|
||||
- [ ] key backup/recovery (lose key = lose data)
|
||||
|
||||
plugin hooks (once core exists)
|
||||
- [ ] on_access_grant_request for custom authorization
|
||||
- [ ] on_key_rotation to notify interested parties
|
||||
|
||||
@@ -468,9 +468,16 @@ impl ServerConfig {
|
||||
/// Returns the user handle domains, falling back to `[hostname_without_port]`.
|
||||
pub fn user_handle_domain_list(&self) -> Vec<String> {
|
||||
self.user_handle_domains
|
||||
.clone()
|
||||
.as_deref()
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(|v| v.to_vec())
|
||||
.unwrap_or_else(|| vec![self.hostname_without_port().to_string()])
|
||||
}
|
||||
|
||||
/// Alias for `user_handle_domain_list` (for callers that were using the now-removed `available_user_domains` field).
|
||||
pub fn available_user_domain_list(&self) -> Vec<String> {
|
||||
self.user_handle_domain_list()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
|
||||
@@ -69,9 +69,9 @@ pub async fn update_account_handle(
|
||||
{
|
||||
return Err(ApiError::InvalidHandle(None));
|
||||
}
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let handle = if !input_handle.contains('.') {
|
||||
format!("{}.{}", input_handle, hostname_for_handles)
|
||||
format!("{}.{}", input_handle, &available_domains[0])
|
||||
} else {
|
||||
input_handle.to_string()
|
||||
};
|
||||
|
||||
@@ -435,20 +435,22 @@ pub async fn create_delegated_account(
|
||||
};
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_handles);
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
|
||||
let handle_to_validate = if input.handle.ends_with(&pds_suffix) {
|
||||
input
|
||||
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&pds_suffix)
|
||||
.unwrap_or(&input.handle)
|
||||
} else {
|
||||
&input.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, hostname_for_handles),
|
||||
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(e) => {
|
||||
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
|
||||
}
|
||||
|
||||
@@ -140,19 +140,21 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
|
||||
let hostname_for_validation = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_validation);
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let validated_short_handle = if !input.handle.contains('.')
|
||||
|| input.handle.ends_with(&pds_suffix)
|
||||
|| matched_domain.is_some()
|
||||
{
|
||||
let handle_to_validate = if input.handle.ends_with(&pds_suffix) {
|
||||
input
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&pds_suffix)
|
||||
.unwrap_or(&input.handle)
|
||||
} else {
|
||||
&input.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => h,
|
||||
@@ -233,15 +235,11 @@ pub async fn create_account(
|
||||
})
|
||||
};
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let suffix = format!(".{}", hostname_for_handles);
|
||||
let handle = if input.handle.ends_with(&suffix) {
|
||||
format!("{}.{}", validated_short_handle, hostname_for_handles)
|
||||
} else if input.handle.contains('.') {
|
||||
validated_short_handle.clone()
|
||||
} else {
|
||||
format!("{}.{}", validated_short_handle, hostname_for_handles)
|
||||
let handle = match matched_domain {
|
||||
Some(domain) => format!("{}.{}", validated_short_handle, domain),
|
||||
None if input.handle.contains('.') => validated_short_handle.clone(),
|
||||
None => format!("{}.{}", validated_short_handle, &available_domains[0]),
|
||||
};
|
||||
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
|
||||
if let Some(signing_key_did) = &input.signing_key {
|
||||
@@ -276,7 +274,8 @@ pub async fn create_account(
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
let subdomain_host = format!("{}.{}", input.handle, hostname_for_handles);
|
||||
let pds_hostname = tranquil_config::get().server.hostname_without_port();
|
||||
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
|
||||
info!(did = %self_hosted_did, "Creating self-hosted did:web account (subdomain)");
|
||||
|
||||
@@ -675,20 +675,24 @@ pub async fn update_handle(
|
||||
"Inappropriate language in handle".into(),
|
||||
)));
|
||||
}
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let suffix = format!(".{}", hostname_for_handles);
|
||||
let is_service_domain =
|
||||
crate::handle::is_service_domain_handle(&new_handle, hostname_for_handles);
|
||||
let handle = if is_service_domain && new_handle != hostname_for_handles {
|
||||
let short_part = if new_handle.ends_with(&suffix) {
|
||||
new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
|
||||
} else {
|
||||
&new_handle
|
||||
};
|
||||
let full_handle = if new_handle.ends_with(&suffix) {
|
||||
new_handle.clone()
|
||||
} else {
|
||||
format!("{}.{}", new_handle, hostname_for_handles)
|
||||
let handle_domains = tranquil_config::get().server.user_handle_domain_list();
|
||||
let matched_handle_domain = handle_domains
|
||||
.iter()
|
||||
.filter(|d| new_handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len())
|
||||
.cloned();
|
||||
let is_domain_itself = handle_domains.iter().any(|d| d == &new_handle);
|
||||
let handle = if (!new_handle.contains('.') || matched_handle_domain.is_some()) && !is_domain_itself {
|
||||
let (short_part, full_handle) = match &matched_handle_domain {
|
||||
Some(domain) => {
|
||||
let suffix = format!(".{}", domain);
|
||||
let short = new_handle.strip_suffix(&suffix).unwrap_or(&new_handle);
|
||||
(short.to_string(), new_handle.clone())
|
||||
}
|
||||
None => {
|
||||
let primary = &handle_domains[0];
|
||||
(new_handle.clone(), format!("{}.{}", new_handle, primary))
|
||||
}
|
||||
};
|
||||
if full_handle == current_handle {
|
||||
let handle_typed: Handle = match full_handle.parse() {
|
||||
|
||||
@@ -113,20 +113,22 @@ pub async fn create_passkey_account(
|
||||
.unwrap_or(false);
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let pds_suffix = format!(".{}", hostname_for_handles);
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) {
|
||||
let handle_to_validate = if input.handle.ends_with(&pds_suffix) {
|
||||
input
|
||||
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&pds_suffix)
|
||||
.unwrap_or(&input.handle)
|
||||
} else {
|
||||
&input.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, hostname_for_handles),
|
||||
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(None).into_response();
|
||||
}
|
||||
@@ -244,7 +246,8 @@ pub async fn create_passkey_account(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
let subdomain_host = format!("{}.{}", input.handle, hostname_for_handles);
|
||||
let pds_hostname = tranquil_config::get().server.hostname_without_port();
|
||||
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
|
||||
info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account");
|
||||
|
||||
@@ -772,8 +772,8 @@ pub async fn check_handle_available(
|
||||
}
|
||||
};
|
||||
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let full_handle = format!("{}.{}", validated, hostname_for_handles);
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let full_handle = format!("{}.{}", validated, &available_domains[0]);
|
||||
let handle_typed: crate::types::Handle = match full_handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
@@ -856,10 +856,10 @@ pub async fn complete_registration(
|
||||
.ok_or(ApiError::SsoSessionExpired)?;
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
|
||||
let handle = match crate::api::validation::validate_short_handle(&input.handle) {
|
||||
Ok(h) => format!("{}.{}", h, hostname_for_handles),
|
||||
Ok(h) => format!("{}.{}", h, &available_domains[0]),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
|
||||
@@ -981,7 +981,8 @@ pub async fn complete_registration(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
let subdomain_host = format!("{}.{}", input.handle, hostname_for_handles);
|
||||
let pds_hostname = tranquil_config::get().server.hostname_without_port();
|
||||
let subdomain_host = format!("{}.{}", input.handle, pds_hostname);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_subdomain);
|
||||
tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account");
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const HANDLE_DOMAIN: &str = "handles.test";
|
||||
|
||||
fn set_handle_domain() {
|
||||
unsafe {
|
||||
std::env::set_var("AVAILABLE_USER_DOMAINS", HANDLE_DOMAIN);
|
||||
std::env::set_var("PDS_USER_HANDLE_DOMAINS", HANDLE_DOMAIN);
|
||||
}
|
||||
}
|
||||
|
||||
async fn base_url_with_domain() -> &'static str {
|
||||
set_handle_domain();
|
||||
base_url().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn describe_server_returns_configured_domain() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.server.describeServer",
|
||||
base
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("describeServer request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let domains = body["availableUserDomains"]
|
||||
.as_array()
|
||||
.expect("No availableUserDomains");
|
||||
assert!(
|
||||
domains.iter().any(|d| d.as_str() == Some(HANDLE_DOMAIN)),
|
||||
"availableUserDomains should contain {}, got {:?}",
|
||||
HANDLE_DOMAIN,
|
||||
domains
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn short_handle_uses_configured_domain() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let payload = json!({
|
||||
"handle": short_handle,
|
||||
"email": format!("{}@example.com", short_handle),
|
||||
"password": "Testpass123!"
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("createAccount request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let handle = body["handle"].as_str().expect("No handle in response");
|
||||
let expected_suffix = format!(".{}", HANDLE_DOMAIN);
|
||||
assert!(
|
||||
handle.ends_with(&expected_suffix),
|
||||
"Handle '{}' should end with '{}' (not PDS hostname)",
|
||||
handle,
|
||||
expected_suffix
|
||||
);
|
||||
assert_eq!(
|
||||
handle,
|
||||
format!("{}.{}", short_handle, HANDLE_DOMAIN),
|
||||
"Handle should be short_handle.configured_domain"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_handle_with_configured_domain_accepted() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let full_handle = format!("{}.{}", short_handle, HANDLE_DOMAIN);
|
||||
let payload = json!({
|
||||
"handle": full_handle,
|
||||
"email": format!("{}@example.com", short_handle),
|
||||
"password": "Testpass123!"
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("createAccount request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let handle = body["handle"].as_str().expect("No handle in response");
|
||||
assert_eq!(
|
||||
handle, full_handle,
|
||||
"Handle should match the full handle submitted"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_with_pds_hostname_treated_as_custom() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let pds_hostname = pds_hostname();
|
||||
let pds_host_no_port = pds_hostname.split(':').next().unwrap_or(&pds_hostname);
|
||||
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let handle_with_hostname = format!("{}.{}", short_handle, pds_host_no_port);
|
||||
let payload = json!({
|
||||
"handle": handle_with_hostname,
|
||||
"email": format!("{}@example.com", short_handle),
|
||||
"password": "Testpass123!"
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("createAccount request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let handle = body["handle"].as_str().expect("No handle in response");
|
||||
assert_eq!(
|
||||
handle, handle_with_hostname,
|
||||
"Handle with non-available domain suffix should be treated as custom handle (passed through)"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_handle_works_with_configured_domain() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let payload = json!({
|
||||
"handle": short_handle,
|
||||
"email": format!("{}@example.com", short_handle),
|
||||
"password": "Testpass123!"
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("createAccount request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let did = body["did"].as_str().expect("No DID").to_string();
|
||||
let full_handle = body["handle"].as_str().expect("No handle").to_string();
|
||||
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.identity.resolveHandle",
|
||||
base
|
||||
))
|
||||
.query(&[("handle", full_handle.as_str())])
|
||||
.send()
|
||||
.await
|
||||
.expect("resolveHandle request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["did"], did);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_update_handle_uses_configured_domain() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
|
||||
let (_, target_did) = create_account_and_login(&client).await;
|
||||
|
||||
let new_short = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.admin.updateAccountHandle",
|
||||
base
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.json(&json!({
|
||||
"did": target_did,
|
||||
"handle": new_short,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("admin updateAccountHandle request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.identity.resolveHandle",
|
||||
base
|
||||
))
|
||||
.query(&[("handle", format!("{}.{}", new_short, HANDLE_DOMAIN))])
|
||||
.send()
|
||||
.await
|
||||
.expect("resolveHandle request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(
|
||||
body["did"], target_did,
|
||||
"Admin bare handle update should use configured domain, not PDS hostname"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_handle_bare_uses_configured_domain() {
|
||||
let client = client();
|
||||
let base = base_url_with_domain().await;
|
||||
let short_handle = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let payload = json!({
|
||||
"handle": short_handle,
|
||||
"email": format!("{}@example.com", short_handle),
|
||||
"password": "Testpass123!"
|
||||
});
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base
|
||||
))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("createAccount request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let did = body["did"].as_str().expect("No DID").to_string();
|
||||
let access_jwt = verify_new_account(&client, &did).await;
|
||||
|
||||
let new_short = format!("hd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.identity.updateHandle",
|
||||
base
|
||||
))
|
||||
.bearer_auth(&access_jwt)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.json(&json!({ "handle": new_short }))
|
||||
.send()
|
||||
.await
|
||||
.expect("updateHandle request failed");
|
||||
assert_eq!(
|
||||
res.status(),
|
||||
StatusCode::OK,
|
||||
"updateHandle failed: {:?}",
|
||||
res.text().await
|
||||
);
|
||||
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.identity.resolveHandle",
|
||||
base
|
||||
))
|
||||
.query(&[("handle", format!("{}.{}", new_short, HANDLE_DOMAIN))])
|
||||
.send()
|
||||
.await
|
||||
.expect("resolveHandle request failed");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(
|
||||
body["did"], did,
|
||||
"updateHandle with bare handle should use configured domain, not PDS hostname"
|
||||
);
|
||||
}
|
||||
@@ -428,7 +428,7 @@
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>Made by people who don't take themselves too seriously</span>
|
||||
<span>Open Source: issues & PRs welcome</span>
|
||||
<span>Open source & open hearts</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -485,14 +485,7 @@
|
||||
})
|
||||
.then(function (info) {
|
||||
var hostnameEl = document.getElementById("hostname");
|
||||
if (
|
||||
info.availableUserDomains &&
|
||||
info.availableUserDomains.length
|
||||
) {
|
||||
hostnameEl.textContent = info.availableUserDomains[0];
|
||||
} else {
|
||||
hostnameEl.textContent = "Tranquil PDS";
|
||||
}
|
||||
hostnameEl.textContent = window.location.hostname;
|
||||
hostnameEl.classList.remove("placeholder");
|
||||
if (info.version) {
|
||||
document.getElementById("version").textContent =
|
||||
@@ -501,7 +494,7 @@
|
||||
})
|
||||
.catch(function () {
|
||||
var hostnameEl = document.getElementById("hostname");
|
||||
hostnameEl.textContent = "Tranquil PDS";
|
||||
hostnameEl.textContent = window.location.hostname;
|
||||
hostnameEl.classList.remove("placeholder");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value: string
|
||||
domains: string[]
|
||||
selectedDomain: string
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
id?: string
|
||||
autocomplete?: string
|
||||
onInput: (value: string) => void
|
||||
onDomainChange: (domain: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
value,
|
||||
domains,
|
||||
selectedDomain,
|
||||
disabled = false,
|
||||
placeholder = 'username',
|
||||
id = 'handle',
|
||||
autocomplete = 'off',
|
||||
onInput,
|
||||
onDomainChange,
|
||||
}: Props = $props()
|
||||
|
||||
const showDomainSelect = $derived(domains.length > 1 && !value.includes('.'))
|
||||
</script>
|
||||
|
||||
<div class="handle-input-group">
|
||||
<input
|
||||
{id}
|
||||
type="text"
|
||||
value={value}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
autocomplete={autocomplete}
|
||||
required
|
||||
oninput={(e) => onInput((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{#if showDomainSelect}
|
||||
<select value={selectedDomain} onchange={(e) => onDomainChange((e.target as HTMLSelectElement).value)}>
|
||||
{#each domains as domain}
|
||||
<option value={domain}>.{domain}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if domains.length === 1 && !value.includes('.')}
|
||||
<span class="domain-suffix">.{domains[0]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.handle-input-group {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.handle-input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.handle-input-group select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.domain-suffix {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
import { getSessionEmail } from '../../lib/types/api'
|
||||
import { formatDate } from '../../lib/date'
|
||||
import { navigate, routes } from '../../lib/router.svelte'
|
||||
import HandleInput from '../HandleInput.svelte'
|
||||
|
||||
interface Props {
|
||||
session: Session
|
||||
@@ -17,14 +18,17 @@
|
||||
let { session }: Props = $props()
|
||||
|
||||
const supportedLocales = getSupportedLocales()
|
||||
let pdsHostname = $state<string | null>(null)
|
||||
let availableDomains = $state<string[]>([])
|
||||
let selectedDomain = $state('')
|
||||
let pdsHostname = $derived(selectedDomain || null)
|
||||
|
||||
onMount(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const info = await api.describeServer()
|
||||
if (info.availableUserDomains?.length) {
|
||||
pdsHostname = info.availableUserDomains[0]
|
||||
availableDomains = info.availableUserDomains
|
||||
selectedDomain = info.availableUserDomains[0]
|
||||
}
|
||||
} catch {}
|
||||
loadBackups()
|
||||
@@ -150,7 +154,7 @@
|
||||
if (!newHandle) return
|
||||
handleLoading = true
|
||||
try {
|
||||
const fullHandle = showBYOHandle ? newHandle : `${newHandle}.${pdsHostname}`
|
||||
const fullHandle = showBYOHandle ? newHandle : `${newHandle}.${selectedDomain}`
|
||||
await api.updateHandle(session.accessJwt, unsafeAsHandle(fullHandle))
|
||||
await refreshSession()
|
||||
toast.success($_('settings.messages.handleUpdated'))
|
||||
@@ -481,12 +485,18 @@
|
||||
<form onsubmit={handleUpdateHandle}>
|
||||
<div class="field">
|
||||
<label for="new-handle">{$_('settings.newHandle')}</label>
|
||||
<div class="handle-input-wrapper">
|
||||
<input id="new-handle" type="text" bind:value={newHandle} placeholder={$_('settings.newHandlePlaceholder')} disabled={handleLoading} required />
|
||||
<span class="handle-suffix">.{pdsHostname ?? '...'}</span>
|
||||
</div>
|
||||
<HandleInput
|
||||
id="new-handle"
|
||||
value={newHandle}
|
||||
domains={availableDomains}
|
||||
{selectedDomain}
|
||||
placeholder={$_('settings.newHandlePlaceholder')}
|
||||
disabled={handleLoading}
|
||||
onInput={(v) => { newHandle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={handleLoading || !newHandle || !pdsHostname}>
|
||||
<button type="submit" disabled={handleLoading || !newHandle || !selectedDomain}>
|
||||
{handleLoading ? $_('settings.updating') : $_('settings.changeHandleButton')}
|
||||
</button>
|
||||
</form>
|
||||
@@ -689,36 +699,6 @@
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.handle-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.handle-input-wrapper input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.handle-input-wrapper input:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.handle-suffix {
|
||||
padding: 0 var(--space-3);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
white-space: nowrap;
|
||||
border-left: 1px solid var(--border-color);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { AuthMethod, HandlePreservation, ServerDescription } from '../../lib/migration/types'
|
||||
import { _ } from '../../lib/i18n'
|
||||
import HandleInput from '../HandleInput.svelte'
|
||||
|
||||
interface Props {
|
||||
handleInput: string
|
||||
@@ -171,23 +172,15 @@
|
||||
{:else}
|
||||
<div class="field">
|
||||
<label for="new-handle">{$_('migration.inbound.chooseHandle.newHandle')}</label>
|
||||
<div class="handle-input-group">
|
||||
<input
|
||||
id="new-handle"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
value={handleInput}
|
||||
oninput={(e) => onHandleChange((e.target as HTMLInputElement).value)}
|
||||
onblur={onCheckHandle}
|
||||
/>
|
||||
{#if serverInfo && serverInfo.availableUserDomains.length > 0 && !handleInput.includes('.')}
|
||||
<select value={selectedDomain} onchange={(e) => onDomainChange((e.target as HTMLSelectElement).value)}>
|
||||
{#each serverInfo.availableUserDomains as domain}
|
||||
<option value={domain}>.{domain}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
<HandleInput
|
||||
id="new-handle"
|
||||
value={handleInput}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder="username"
|
||||
onInput={onHandleChange}
|
||||
onDomainChange={onDomainChange}
|
||||
/>
|
||||
|
||||
{#if handleTooShort}
|
||||
<p class="hint error">{$_('migration.inbound.chooseHandle.handleTooShort')}</p>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
type WebAuthnCreationOptionsResponse,
|
||||
} from '../lib/webauthn'
|
||||
import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte'
|
||||
import HandleInput from '../components/HandleInput.svelte'
|
||||
|
||||
let serverInfo = $state<{
|
||||
availableUserDomains: string[]
|
||||
@@ -30,6 +31,7 @@
|
||||
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
|
||||
let passkeyName = $state('')
|
||||
let clientName = $state<string | null>(null)
|
||||
let selectedDomain = $state('')
|
||||
|
||||
function getRequestUri(): string | null {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -99,6 +101,7 @@
|
||||
const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
flow = createRegistrationFlow('passkey', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -262,7 +265,8 @@
|
||||
|
||||
let fullHandle = $derived(() => {
|
||||
if (!flow?.info.handle.trim()) return ''
|
||||
return `${flow.info.handle.trim()}.${flow.state.pdsHostname}`
|
||||
if (flow.info.handle.includes('.')) return flow.info.handle.trim()
|
||||
return selectedDomain ? `${flow.info.handle.trim()}.${selectedDomain}` : flow.info.handle.trim()
|
||||
})
|
||||
|
||||
async function handleCancel() {
|
||||
@@ -342,14 +346,14 @@
|
||||
<form onsubmit={handleInfoSubmit}>
|
||||
<div class="field">
|
||||
<label for="handle">{$_('register.handle')}</label>
|
||||
<input
|
||||
id="handle"
|
||||
type="text"
|
||||
bind:value={flow.info.handle}
|
||||
<HandleInput
|
||||
value={flow.info.handle}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
required
|
||||
autocomplete="off"
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
{#if fullHandle()}
|
||||
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { _ } from '../lib/i18n'
|
||||
import { toast } from '../lib/toast.svelte'
|
||||
import SsoIcon from '../components/SsoIcon.svelte'
|
||||
import HandleInput from '../components/HandleInput.svelte'
|
||||
|
||||
interface PendingRegistration {
|
||||
request_uri: string
|
||||
@@ -37,6 +38,7 @@
|
||||
let handleAvailable = $state<boolean | null>(null)
|
||||
let checkingHandle = $state(false)
|
||||
let handleError = $state<string | null>(null)
|
||||
let selectedDomain = $state('')
|
||||
|
||||
let didType = $state<'plc' | 'web' | 'web-external'>('plc')
|
||||
let externalDid = $state('')
|
||||
@@ -80,8 +82,8 @@
|
||||
|
||||
let fullHandle = $derived(() => {
|
||||
if (!handle.trim()) return ''
|
||||
const domain = serverInfo?.availableUserDomains?.[0]
|
||||
return domain ? `${handle.trim()}.${domain}` : handle.trim()
|
||||
if (handle.includes('.')) return handle.trim()
|
||||
return selectedDomain ? `${handle.trim()}.${selectedDomain}` : handle.trim()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -106,6 +108,7 @@
|
||||
telegram: available.includes('telegram'),
|
||||
signal: available.includes('signal'),
|
||||
}
|
||||
selectedDomain = data.availableUserDomains?.[0] || window.location.hostname
|
||||
}
|
||||
} catch {
|
||||
serverInfo = null
|
||||
@@ -317,14 +320,14 @@
|
||||
<form onsubmit={handleSubmit}>
|
||||
<div class="field">
|
||||
<label for="handle">{$_('sso_register.handle_label')}</label>
|
||||
<input
|
||||
id="handle"
|
||||
type="text"
|
||||
bind:value={handle}
|
||||
<HandleInput
|
||||
value={handle}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={submitting}
|
||||
required
|
||||
autocomplete="off"
|
||||
onInput={(v) => { handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
{#if checkingHandle}
|
||||
<p class="hint">{$_('common.checking')}</p>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
type WebAuthnCreationOptionsResponse,
|
||||
} from '../lib/webauthn'
|
||||
import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte'
|
||||
import HandleInput from '../components/HandleInput.svelte'
|
||||
import { ensureRequestUri, getRequestUriFromUrl } from '../lib/oauth'
|
||||
|
||||
let serverInfo = $state<{
|
||||
@@ -31,6 +32,7 @@
|
||||
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
|
||||
let passkeyName = $state('')
|
||||
let clientName = $state<string | null>(null)
|
||||
let selectedDomain = $state('')
|
||||
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
$effect(() => {
|
||||
@@ -112,6 +114,7 @@
|
||||
const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
flow = createRegistrationFlow('passkey', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -276,7 +279,8 @@
|
||||
|
||||
let fullHandle = $derived(() => {
|
||||
if (!flow?.info.handle.trim()) return ''
|
||||
return `${flow.info.handle.trim()}.${flow.state.pdsHostname}`
|
||||
if (flow.info.handle.includes('.')) return flow.info.handle.trim()
|
||||
return selectedDomain ? `${flow.info.handle.trim()}.${selectedDomain}` : flow.info.handle.trim()
|
||||
})
|
||||
|
||||
async function handleCancel() {
|
||||
@@ -357,14 +361,14 @@
|
||||
<form class="register-form" onsubmit={handleInfoSubmit}>
|
||||
<div class="field">
|
||||
<label for="handle">{$_('register.handle')}</label>
|
||||
<input
|
||||
id="handle"
|
||||
type="text"
|
||||
bind:value={flow.info.handle}
|
||||
<HandleInput
|
||||
value={flow.info.handle}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
required
|
||||
autocomplete="off"
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
{#if flow.info.handle.includes('.')}
|
||||
<p class="hint warning">{$_('register.handleDotWarning')}</p>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
DidDocStep,
|
||||
} from '../lib/registration'
|
||||
import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte'
|
||||
import HandleInput from '../components/HandleInput.svelte'
|
||||
import { ensureRequestUri, getRequestUriFromUrl } from '../lib/oauth'
|
||||
|
||||
let serverInfo = $state<{
|
||||
@@ -25,6 +26,7 @@
|
||||
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
|
||||
let confirmPassword = $state('')
|
||||
let clientName = $state<string | null>(null)
|
||||
let selectedDomain = $state('')
|
||||
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
$effect(() => {
|
||||
@@ -106,6 +108,7 @@
|
||||
const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
flow = createRegistrationFlow('password', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -229,9 +232,7 @@
|
||||
let fullHandle = $derived(() => {
|
||||
if (!flow?.info.handle.trim()) return ''
|
||||
if (flow.info.handle.includes('.')) return flow.info.handle.trim()
|
||||
const domain = serverInfo?.availableUserDomains?.[0]
|
||||
if (domain) return `${flow.info.handle.trim()}.${domain}`
|
||||
return flow.info.handle.trim()
|
||||
return selectedDomain ? `${flow.info.handle.trim()}.${selectedDomain}` : flow.info.handle.trim()
|
||||
})
|
||||
|
||||
function extractDomain(did: string): string {
|
||||
@@ -305,13 +306,14 @@
|
||||
<form class="register-form" onsubmit={handleInfoSubmit}>
|
||||
<div class="field">
|
||||
<label for="handle">{$_('register.handle')}</label>
|
||||
<input
|
||||
id="handle"
|
||||
type="text"
|
||||
bind:value={flow.info.handle}
|
||||
<HandleInput
|
||||
value={flow.info.handle}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
required
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
{#if flow.info.handle.includes('.')}
|
||||
<p class="hint warning">{$_('register.handleDotWarning')}</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { _ } from '../lib/i18n'
|
||||
import { toast } from '../lib/toast.svelte'
|
||||
import SsoIcon from '../components/SsoIcon.svelte'
|
||||
import HandleInput from '../components/HandleInput.svelte'
|
||||
|
||||
interface PendingRegistration {
|
||||
request_uri: string
|
||||
@@ -47,6 +48,7 @@
|
||||
let handleAvailable = $state<boolean | null>(null)
|
||||
let checkingHandle = $state(false)
|
||||
let handleError = $state<string | null>(null)
|
||||
let selectedDomain = $state('')
|
||||
|
||||
let didType = $state<'plc' | 'web' | 'web-external'>('plc')
|
||||
let externalDid = $state('')
|
||||
@@ -95,8 +97,8 @@
|
||||
|
||||
let fullHandle = $derived(() => {
|
||||
if (!handle.trim()) return ''
|
||||
const domain = serverInfo?.availableUserDomains?.[0]
|
||||
return domain ? `${handle.trim()}.${domain}` : handle.trim()
|
||||
if (handle.includes('.')) return handle.trim()
|
||||
return selectedDomain ? `${handle.trim()}.${selectedDomain}` : handle.trim()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -121,6 +123,7 @@
|
||||
telegram: available.includes('telegram'),
|
||||
signal: available.includes('signal'),
|
||||
}
|
||||
selectedDomain = data.availableUserDomains?.[0] || window.location.hostname
|
||||
}
|
||||
} catch {
|
||||
serverInfo = null
|
||||
@@ -390,14 +393,14 @@
|
||||
<form onsubmit={handleSubmit}>
|
||||
<div class="field">
|
||||
<label for="handle">{$_('sso_register.handle_label')}</label>
|
||||
<input
|
||||
id="handle"
|
||||
type="text"
|
||||
bind:value={handle}
|
||||
<HandleInput
|
||||
value={handle}
|
||||
domains={serverInfo?.availableUserDomains ?? []}
|
||||
{selectedDomain}
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={submitting}
|
||||
required
|
||||
autocomplete="off"
|
||||
onInput={(v) => { handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
/>
|
||||
{#if checkingHandle}
|
||||
<p class="hint">{$_('common.checking')}</p>
|
||||
|
||||
@@ -170,19 +170,6 @@
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
.handle-input-group {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.handle-input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.handle-input-group select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.current-info {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
Reference in New Issue
Block a user