mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-04 17:26:56 +00:00
fix: did:web also uses handle domains not hostname
This commit is contained in:
Generated
+15
-15
@@ -6094,7 +6094,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-auth"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base32",
|
||||
@@ -6117,7 +6117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-cache"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6131,7 +6131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-comms"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6146,7 +6146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-config"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"confique",
|
||||
"serde",
|
||||
@@ -6154,7 +6154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-crypto"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
@@ -6170,7 +6170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -6187,7 +6187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db-traits"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6203,7 +6203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-infra"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -6214,7 +6214,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -6237,7 +6237,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-pds"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -6324,7 +6324,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-repo"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cid",
|
||||
@@ -6336,7 +6336,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-ripple"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
@@ -6361,7 +6361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-scopes"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
@@ -6377,7 +6377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-storage"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
@@ -6394,7 +6394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-types"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cid",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
|
||||
@@ -140,7 +140,8 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let cfg = tranquil_config::get();
|
||||
let available_domains = cfg.server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
@@ -163,23 +164,10 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if input.handle.contains(' ') || input.handle.contains('\t') {
|
||||
return ApiError::InvalidRequest("Handle cannot contain spaces".into()).into_response();
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
}
|
||||
if let Some(c) = input
|
||||
.handle
|
||||
.chars()
|
||||
.find(|c| !c.is_ascii_alphanumeric() && *c != '.' && *c != '-')
|
||||
{
|
||||
return ApiError::InvalidRequest(format!("Handle contains invalid character: {}", c))
|
||||
.into_response();
|
||||
}
|
||||
let handle_lower = input.handle.to_lowercase();
|
||||
if crate::moderation::has_explicit_slur(&handle_lower) {
|
||||
return ApiError::InvalidRequest("Inappropriate language in handle".into())
|
||||
.into_response();
|
||||
}
|
||||
handle_lower
|
||||
};
|
||||
let email: Option<String> = input
|
||||
.email
|
||||
@@ -234,7 +222,7 @@ pub async fn create_account(
|
||||
},
|
||||
})
|
||||
};
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname = &cfg.server.hostname;
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let handle = match matched_domain {
|
||||
Some(domain) => format!("{}.{}", validated_short_handle, domain),
|
||||
@@ -274,10 +262,8 @@ pub async fn create_account(
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
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);
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_handle);
|
||||
info!(did = %self_hosted_did, "Creating self-hosted did:web account (subdomain)");
|
||||
self_hosted_did
|
||||
}
|
||||
|
||||
@@ -122,17 +122,21 @@ pub fn get_public_key_multibase(key_bytes: &[u8]) -> Result<String, KeyError> {
|
||||
}
|
||||
|
||||
pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let hostname_without_port = tranquil_config::get().server.hostname_without_port();
|
||||
let cfg = tranquil_config::get();
|
||||
let hostname = &cfg.server.hostname;
|
||||
let hostname_without_port = cfg.server.hostname_without_port();
|
||||
let host_header = get_header_str(&headers, http::header::HOST).unwrap_or(hostname);
|
||||
let host_without_port = host_header.split(':').next().unwrap_or(host_header);
|
||||
if host_without_port != hostname_without_port
|
||||
&& host_without_port.ends_with(&format!(".{}", hostname_without_port))
|
||||
{
|
||||
let handle = host_without_port
|
||||
.strip_suffix(&format!(".{}", hostname_without_port))
|
||||
.unwrap_or(host_without_port);
|
||||
return serve_subdomain_did_doc(&state, handle, hostname).await;
|
||||
if host_without_port != hostname_without_port {
|
||||
let is_subdomain = cfg
|
||||
.server
|
||||
.available_user_domain_list()
|
||||
.into_iter()
|
||||
.chain(std::iter::once(hostname_without_port.to_string()))
|
||||
.any(|d| host_without_port.ends_with(&format!(".{}", d)));
|
||||
if is_subdomain {
|
||||
return serve_handle_did_doc(&state, host_without_port, hostname).await;
|
||||
}
|
||||
}
|
||||
let did = if hostname.contains(':') {
|
||||
format!("did:web:{}", hostname.replace(':', "%3A"))
|
||||
@@ -151,11 +155,9 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn serve_subdomain_did_doc(state: &AppState, subdomain: &str, hostname: &str) -> Response {
|
||||
let hostname_for_handles = hostname.split(':').next().unwrap_or(hostname);
|
||||
let subdomain_host = format!("{}.{}", subdomain, hostname_for_handles);
|
||||
let encoded_subdomain = subdomain_host.replace(':', "%3A");
|
||||
let expected_did = format!("did:web:{}", encoded_subdomain);
|
||||
async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let expected_did = format!("did:web:{}", encoded_handle);
|
||||
let expected_did_typed: crate::types::Did = match expected_did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
|
||||
@@ -215,15 +215,12 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_ssrf_safe_https() {
|
||||
assert!(is_ssrf_safe("https://api.bsky.app/xrpc/test").is_ok());
|
||||
assert!(is_ssrf_safe("https://1.1.1.1/xrpc/test").is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn test_ssrf_blocks_http_by_default() {
|
||||
let result = is_ssrf_safe("http://external.example.com/xrpc/test");
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(SsrfError::InsecureProtocol(_)) | Err(SsrfError::DnsResolutionFailed(_))
|
||||
));
|
||||
let result = is_ssrf_safe("http://93.184.216.34/xrpc/test");
|
||||
assert!(matches!(result, Err(SsrfError::InsecureProtocol(_))));
|
||||
}
|
||||
#[test]
|
||||
fn test_ssrf_allows_localhost_http() {
|
||||
@@ -231,6 +228,21 @@ mod tests {
|
||||
assert!(is_ssrf_safe("http://localhost:8080/test").is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn test_ssrf_blocks_non_unicast_ip() {
|
||||
assert!(matches!(
|
||||
is_ssrf_safe("https://0.0.0.0/test"),
|
||||
Err(SsrfError::NonUnicastIp(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
is_ssrf_safe("https://224.0.0.1/test"),
|
||||
Err(SsrfError::NonUnicastIp(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
is_ssrf_safe("https://255.255.255.255/test"),
|
||||
Err(SsrfError::NonUnicastIp(_))
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn test_validate_at_uri() {
|
||||
let result = validate_at_uri("at://did:plc:test/app.bsky.feed.post/abc123");
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -112,8 +112,9 @@ pub async fn create_passkey_account(
|
||||
.map(|d| d.starts_with("did:web:"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let cfg = tranquil_config::get();
|
||||
let hostname = &cfg.server.hostname;
|
||||
let available_domains = cfg.server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
@@ -134,7 +135,10 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input.handle.to_lowercase()
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
}
|
||||
};
|
||||
|
||||
let email = input
|
||||
@@ -246,10 +250,11 @@ pub async fn create_passkey_account(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
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);
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_handle);
|
||||
info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account");
|
||||
self_hosted_did
|
||||
}
|
||||
|
||||
@@ -258,6 +258,49 @@ pub enum ReservedHandlePolicy {
|
||||
Reject,
|
||||
}
|
||||
|
||||
pub fn validate_full_domain_handle(handle: &str) -> Result<String, HandleValidationError> {
|
||||
let handle = handle.trim();
|
||||
|
||||
if handle.is_empty() {
|
||||
return Err(HandleValidationError::Empty);
|
||||
}
|
||||
|
||||
if handle.contains(' ') || handle.contains('\t') || handle.contains('\n') {
|
||||
return Err(HandleValidationError::ContainsSpaces);
|
||||
}
|
||||
|
||||
if handle.len() > MAX_HANDLE_LENGTH {
|
||||
return Err(HandleValidationError::TooLong);
|
||||
}
|
||||
|
||||
if handle
|
||||
.chars()
|
||||
.any(|c| !c.is_ascii_alphanumeric() && c != '.' && c != '-')
|
||||
{
|
||||
return Err(HandleValidationError::InvalidCharacters);
|
||||
}
|
||||
|
||||
if !handle.contains('.') {
|
||||
return Err(HandleValidationError::InvalidCharacters);
|
||||
}
|
||||
|
||||
let labels: Vec<&str> = handle.split('.').collect();
|
||||
let has_invalid_label = labels
|
||||
.iter()
|
||||
.any(|label| label.is_empty() || label.len() > MAX_DOMAIN_LABEL_LENGTH || label.starts_with('-') || label.ends_with('-'));
|
||||
if has_invalid_label {
|
||||
return Err(HandleValidationError::InvalidCharacters);
|
||||
}
|
||||
|
||||
let handle_lower = handle.to_lowercase();
|
||||
|
||||
if crate::moderation::has_explicit_slur(&handle_lower) {
|
||||
return Err(HandleValidationError::BannedWord);
|
||||
}
|
||||
|
||||
Ok(handle_lower)
|
||||
}
|
||||
|
||||
pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> {
|
||||
validate_service_handle(handle, ReservedHandlePolicy::Reject)
|
||||
}
|
||||
|
||||
@@ -743,6 +743,7 @@ pub async fn get_pending_registration(
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CheckHandleQuery {
|
||||
pub handle: String,
|
||||
pub domain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -773,7 +774,18 @@ pub async fn check_handle_available(
|
||||
};
|
||||
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let full_handle = format!("{}.{}", validated, &available_domains[0]);
|
||||
if let Some(ref d) = query.domain {
|
||||
if !available_domains.iter().any(|ad| ad == d) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Unknown user domain".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let domain = query
|
||||
.domain
|
||||
.as_deref()
|
||||
.unwrap_or(&available_domains[0]);
|
||||
let full_handle = format!("{}.{}", validated, domain);
|
||||
let handle_typed: crate::types::Handle = match full_handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
@@ -855,12 +867,32 @@ pub async fn complete_registration(
|
||||
.await?
|
||||
.ok_or(ApiError::SsoSessionExpired)?;
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let cfg = tranquil_config::get();
|
||||
let hostname = &cfg.server.hostname;
|
||||
let available_domains = cfg.server.available_user_domain_list();
|
||||
|
||||
let handle = match crate::api::validation::validate_short_handle(&input.handle) {
|
||||
Ok(h) => format!("{}.{}", h, &available_domains[0]),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
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('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => 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, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
}
|
||||
} else {
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
}
|
||||
};
|
||||
|
||||
let verification_channel = input
|
||||
@@ -981,10 +1013,11 @@ pub async fn complete_registration(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
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);
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
return Err(ApiError::SelfHostedDidWebDisabled);
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let self_hosted_did = format!("did:web:{}", encoded_handle);
|
||||
tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account");
|
||||
self_hosted_did
|
||||
}
|
||||
|
||||
@@ -133,10 +133,6 @@ pub struct FirehoseConsumer {
|
||||
}
|
||||
|
||||
impl FirehoseConsumer {
|
||||
pub async fn connect(port: u16) -> Self {
|
||||
Self::connect_inner(port, None).await
|
||||
}
|
||||
|
||||
pub async fn connect_with_cursor(port: u16, cursor: i64) -> Self {
|
||||
Self::connect_inner(port, Some(cursor)).await
|
||||
}
|
||||
|
||||
@@ -276,3 +276,37 @@ async fn update_handle_bare_uses_configured_domain() {
|
||||
"updateHandle with bare handle should use configured domain, not PDS hostname"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn did_web_uses_handle_domain_not_hostname() {
|
||||
unsafe {
|
||||
std::env::set_var("ENABLE_PDS_HOSTED_DID_WEB", "true");
|
||||
}
|
||||
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!",
|
||||
"didType": "web"
|
||||
});
|
||||
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 in response");
|
||||
let expected_did = format!("did:web:{}.{}", short_handle, HANDLE_DOMAIN);
|
||||
assert_eq!(
|
||||
did, expected_did,
|
||||
"did:web should use handle domain '{}', not PDS hostname",
|
||||
HANDLE_DOMAIN
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface RegistrationFlowState {
|
||||
error: string | null;
|
||||
submitting: boolean;
|
||||
pdsHostname: string;
|
||||
selectedDomain: string;
|
||||
handleAvailable: boolean | null;
|
||||
checkingHandle: boolean;
|
||||
discordInUse: boolean;
|
||||
@@ -68,6 +69,7 @@ export function createRegistrationFlow(
|
||||
error: null,
|
||||
submitting: false,
|
||||
pdsHostname,
|
||||
selectedDomain: "",
|
||||
handleAvailable: null,
|
||||
checkingHandle: false,
|
||||
discordInUse: false,
|
||||
@@ -84,7 +86,10 @@ export function createRegistrationFlow(
|
||||
}
|
||||
|
||||
function getFullHandle(): string {
|
||||
return `${state.info.handle.trim()}.${state.pdsHostname}`;
|
||||
const handle = state.info.handle.trim();
|
||||
if (handle.includes('.')) return handle;
|
||||
const domain = state.selectedDomain || state.pdsHostname;
|
||||
return `${handle}.${domain}`;
|
||||
}
|
||||
|
||||
function extractDomain(did: string): string {
|
||||
@@ -132,10 +137,10 @@ export function createRegistrationFlow(
|
||||
}
|
||||
state.checkingHandle = true;
|
||||
try {
|
||||
const params = new URLSearchParams({ handle });
|
||||
if (state.selectedDomain) params.set("domain", state.selectedDomain);
|
||||
const response = await fetch(
|
||||
`${getPdsEndpoint()}/oauth/sso/check-handle-available?handle=${
|
||||
encodeURIComponent(handle)
|
||||
}`,
|
||||
`${getPdsEndpoint()}/oauth/sso/check-handle-available?${params}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
state.handleAvailable = data.available === true;
|
||||
@@ -239,7 +244,7 @@ export function createRegistrationFlow(
|
||||
}
|
||||
|
||||
const result = await api.createAccount({
|
||||
handle: state.info.handle.trim(),
|
||||
handle: getFullHandle(),
|
||||
email: state.info.email.trim(),
|
||||
password: state.info.password!,
|
||||
inviteCode: state.info.inviteCode?.trim() || undefined,
|
||||
@@ -291,7 +296,7 @@ export function createRegistrationFlow(
|
||||
}
|
||||
|
||||
const result = await api.createPasskeyAccount({
|
||||
handle: unsafeAsHandle(state.info.handle.trim()),
|
||||
handle: unsafeAsHandle(getFullHandle()),
|
||||
email: state.info.email?.trim()
|
||||
? unsafeAsEmail(state.info.email.trim())
|
||||
: undefined,
|
||||
@@ -532,6 +537,9 @@ export function createRegistrationFlow(
|
||||
getPdsDid,
|
||||
getFullHandle,
|
||||
extractDomain,
|
||||
setSelectedDomain(domain: string) {
|
||||
state.selectedDomain = domain;
|
||||
},
|
||||
|
||||
proceedFromInfo,
|
||||
selectKeyMode,
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
flow = createRegistrationFlow('passkey', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
if (flow) flow.setSelectedDomain(selectedDomain)
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -353,7 +354,7 @@
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
|
||||
/>
|
||||
{#if fullHandle()}
|
||||
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
$effect(() => {
|
||||
void selectedDomain
|
||||
if (checkHandleTimeout) {
|
||||
clearTimeout(checkHandleTimeout)
|
||||
}
|
||||
@@ -167,7 +168,9 @@
|
||||
handleError = null
|
||||
|
||||
try {
|
||||
const response = await fetch(`/oauth/sso/check-handle-available?handle=${encodeURIComponent(handle)}`)
|
||||
const params = new URLSearchParams({ handle })
|
||||
if (selectedDomain) params.set('domain', selectedDomain)
|
||||
const response = await fetch(`/oauth/sso/check-handle-available?${params}`)
|
||||
const data = await response.json()
|
||||
handleAvailable = data.available
|
||||
if (!data.available && data.reason) {
|
||||
@@ -222,6 +225,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const fullHandle = !handle.includes('.') && selectedDomain
|
||||
? `${handle.trim()}.${selectedDomain}`
|
||||
: handle.trim()
|
||||
submitting = true
|
||||
|
||||
try {
|
||||
@@ -233,7 +239,7 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
handle,
|
||||
handle: fullHandle,
|
||||
email: email || null,
|
||||
invite_code: inviteCode || null,
|
||||
verification_channel: verificationChannel,
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
flow = createRegistrationFlow('passkey', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
if (flow) flow.setSelectedDomain(selectedDomain)
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -368,7 +369,7 @@
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
|
||||
/>
|
||||
{#if flow.info.handle.includes('.')}
|
||||
<p class="hint warning">{$_('register.handleDotWarning')}</p>
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
flow = createRegistrationFlow('password', hostname)
|
||||
}
|
||||
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
|
||||
if (flow) flow.setSelectedDomain(selectedDomain)
|
||||
} catch (e) {
|
||||
console.error('Failed to load server info:', e)
|
||||
} finally {
|
||||
@@ -313,7 +314,7 @@
|
||||
placeholder={$_('register.handlePlaceholder')}
|
||||
disabled={flow.state.submitting}
|
||||
onInput={(v) => { flow!.info.handle = v }}
|
||||
onDomainChange={(d) => { selectedDomain = d }}
|
||||
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
|
||||
/>
|
||||
{#if flow.info.handle.includes('.')}
|
||||
<p class="hint warning">{$_('register.handleDotWarning')}</p>
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
$effect(() => {
|
||||
void selectedDomain
|
||||
if (checkHandleTimeout) {
|
||||
clearTimeout(checkHandleTimeout)
|
||||
}
|
||||
@@ -182,7 +183,9 @@
|
||||
handleError = null
|
||||
|
||||
try {
|
||||
const response = await fetch(`/oauth/sso/check-handle-available?handle=${encodeURIComponent(handle)}`)
|
||||
const params = new URLSearchParams({ handle })
|
||||
if (selectedDomain) params.set('domain', selectedDomain)
|
||||
const response = await fetch(`/oauth/sso/check-handle-available?${params}`)
|
||||
const data = await response.json()
|
||||
handleAvailable = data.available
|
||||
if (!data.available && data.reason) {
|
||||
@@ -269,6 +272,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const fullHandle = !handle.includes('.') && selectedDomain
|
||||
? `${handle.trim()}.${selectedDomain}`
|
||||
: handle.trim()
|
||||
submitting = true
|
||||
|
||||
try {
|
||||
@@ -280,7 +286,7 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
handle,
|
||||
handle: fullHandle,
|
||||
email: email || null,
|
||||
invite_code: inviteCode || null,
|
||||
verification_channel: verificationChannel,
|
||||
|
||||
Reference in New Issue
Block a user