+99
-23
src/api/identity/did.rs
+99
-23
src/api/identity/did.rs
···
511
511
let rotation_keys = if auth_user.did.starts_with("did:web:") {
512
512
vec![]
513
513
} else {
514
-
vec![did_key.clone()]
514
+
let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") {
515
+
Ok(key) => key,
516
+
Err(_) => {
517
+
warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation");
518
+
did_key.clone()
519
+
}
520
+
};
521
+
vec![server_rotation_key]
515
522
};
516
523
(
517
524
StatusCode::OK,
···
559
566
return e;
560
567
}
561
568
let did = auth_user.did;
562
-
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
563
-
.fetch_optional(&state.db)
569
+
if !state
570
+
.check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did)
564
571
.await
565
572
{
566
-
Ok(Some(id)) => id,
573
+
return (
574
+
StatusCode::TOO_MANY_REQUESTS,
575
+
Json(json!({"error": "RateLimitExceeded", "message": "Too many handle updates. Try again later."})),
576
+
)
577
+
.into_response();
578
+
}
579
+
if !state
580
+
.check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did)
581
+
.await
582
+
{
583
+
return (
584
+
StatusCode::TOO_MANY_REQUESTS,
585
+
Json(json!({"error": "RateLimitExceeded", "message": "Daily handle update limit exceeded."})),
586
+
)
587
+
.into_response();
588
+
}
589
+
let user_row = match sqlx::query!(
590
+
"SELECT id, handle FROM users WHERE did = $1",
591
+
did
592
+
)
593
+
.fetch_optional(&state.db)
594
+
.await
595
+
{
596
+
Ok(Some(row)) => row,
567
597
_ => return ApiError::InternalError.into_response(),
568
598
};
569
-
let new_handle = input.handle.trim();
599
+
let user_id = user_row.id;
600
+
let current_handle = user_row.handle;
601
+
let new_handle = input.handle.trim().to_ascii_lowercase();
570
602
if new_handle.is_empty() {
571
603
return ApiError::InvalidRequest("handle is required".into()).into_response();
572
604
}
573
605
if !new_handle
574
606
.chars()
575
-
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
607
+
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
576
608
{
577
609
return (
578
610
StatusCode::BAD_REQUEST,
···
582
614
)
583
615
.into_response();
584
616
}
585
-
if crate::moderation::has_explicit_slur(new_handle) {
617
+
for segment in new_handle.split('.') {
618
+
if segment.is_empty() {
619
+
return (
620
+
StatusCode::BAD_REQUEST,
621
+
Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})),
622
+
)
623
+
.into_response();
624
+
}
625
+
if segment.starts_with('-') || segment.ends_with('-') {
626
+
return (
627
+
StatusCode::BAD_REQUEST,
628
+
Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})),
629
+
)
630
+
.into_response();
631
+
}
632
+
}
633
+
if crate::moderation::has_explicit_slur(&new_handle) {
586
634
return (
587
635
StatusCode::BAD_REQUEST,
588
636
Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})),
···
591
639
}
592
640
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
593
641
let suffix = format!(".{}", hostname);
594
-
let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname);
642
+
let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname);
595
643
let handle = if is_service_domain {
596
644
let short_part = if new_handle.ends_with(&suffix) {
597
-
new_handle.strip_suffix(&suffix).unwrap_or(new_handle)
645
+
new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
598
646
} else {
599
-
new_handle
647
+
&new_handle
600
648
};
649
+
let full_handle = if new_handle.ends_with(&suffix) {
650
+
new_handle.clone()
651
+
} else {
652
+
format!("{}.{}", new_handle, hostname)
653
+
};
654
+
if full_handle == current_handle {
655
+
if let Err(e) =
656
+
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
657
+
.await
658
+
{
659
+
warn!("Failed to sequence identity event for handle update: {}", e);
660
+
}
661
+
return (StatusCode::OK, Json(json!({}))).into_response();
662
+
}
601
663
if short_part.contains('.') {
602
664
return (
603
665
StatusCode::BAD_REQUEST,
···
608
670
)
609
671
.into_response();
610
672
}
611
-
if new_handle.ends_with(&suffix) {
612
-
new_handle.to_string()
613
-
} else {
614
-
format!("{}.{}", new_handle, hostname)
673
+
if short_part.len() < 3 {
674
+
return (
675
+
StatusCode::BAD_REQUEST,
676
+
Json(json!({"error": "InvalidHandle", "message": "Handle too short"})),
677
+
)
678
+
.into_response();
679
+
}
680
+
if short_part.len() > 18 {
681
+
return (
682
+
StatusCode::BAD_REQUEST,
683
+
Json(json!({"error": "InvalidHandle", "message": "Handle too long"})),
684
+
)
685
+
.into_response();
615
686
}
687
+
full_handle
616
688
} else {
617
-
match crate::handle::verify_handle_ownership(new_handle, &did).await {
689
+
if new_handle == current_handle {
690
+
if let Err(e) =
691
+
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle))
692
+
.await
693
+
{
694
+
warn!("Failed to sequence identity event for handle update: {}", e);
695
+
}
696
+
return (StatusCode::OK, Json(json!({}))).into_response();
697
+
}
698
+
match crate::handle::verify_handle_ownership(&new_handle, &did).await {
618
699
Ok(()) => {}
619
700
Err(crate::handle::HandleResolutionError::NotFound) => {
620
701
return (
···
649
730
.into_response();
650
731
}
651
732
}
652
-
new_handle.to_string()
733
+
new_handle.clone()
653
734
};
654
-
let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id)
655
-
.fetch_optional(&state.db)
656
-
.await
657
-
.ok()
658
-
.flatten();
659
735
let existing = sqlx::query!(
660
736
"SELECT id FROM users WHERE handle = $1 AND id != $2",
661
737
handle,
···
679
755
.await;
680
756
match result {
681
757
Ok(_) => {
682
-
if let Some(old) = old_handle {
683
-
let _ = state.cache.delete(&format!("handle:{}", old)).await;
758
+
if !current_handle.is_empty() {
759
+
let _ = state.cache.delete(&format!("handle:{}", current_handle)).await;
684
760
}
685
761
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
686
762
if let Err(e) =
+28
-65
src/api/identity/plc/submit.rs
+28
-65
src/api/identity/plc/submit.rs
···
23
23
headers: axum::http::HeaderMap,
24
24
Json(input): Json<SubmitPlcOperationInput>,
25
25
) -> Response {
26
-
info!("[MIGRATION] submitPlcOperation called");
27
26
let bearer = match crate::auth::extract_bearer_token_from_header(
28
27
headers.get("Authorization").and_then(|h| h.to_str().ok()),
29
28
) {
30
29
Some(t) => t,
31
30
None => {
32
-
info!("[MIGRATION] submitPlcOperation: No bearer token");
33
31
return ApiError::AuthenticationRequired.into_response();
34
32
}
35
33
};
···
37
35
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
38
36
Ok(user) => user,
39
37
Err(e) => {
40
-
info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e);
41
38
return ApiError::from(e).into_response();
42
39
}
43
40
};
44
-
info!(
45
-
"[MIGRATION] submitPlcOperation: Authenticated user did={}",
46
-
auth_user.did
47
-
);
48
41
if let Err(e) = crate::auth::scope_check::check_identity_scope(
49
42
auth_user.is_oauth,
50
43
auth_user.scope.as_deref(),
51
44
crate::oauth::scopes::IdentityAttr::Wildcard,
52
45
) {
53
-
info!("[MIGRATION] submitPlcOperation: Scope check failed");
54
46
return e;
55
47
}
56
48
let did = &auth_user.did;
···
67
59
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
68
60
let public_url = format!("https://{}", hostname);
69
61
let user = match sqlx::query!(
70
-
"SELECT id, handle, deactivated_at FROM users WHERE did = $1",
62
+
"SELECT id, handle FROM users WHERE did = $1",
71
63
did
72
64
)
73
65
.fetch_optional(&state.db)
···
82
74
.into_response();
83
75
}
84
76
};
85
-
let is_migration = user.deactivated_at.is_some();
86
77
let key_row = match sqlx::query!(
87
78
"SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
88
79
user.id
···
123
114
}
124
115
};
125
116
let user_did_key = signing_key_to_did_key(&signing_key);
126
-
if !is_migration && let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array())
127
-
{
128
-
let server_rotation_key =
129
-
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
117
+
let server_rotation_key =
118
+
std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone());
119
+
if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) {
130
120
let has_server_key = rotation_keys
131
121
.iter()
132
122
.any(|k| k.as_str() == Some(&server_rotation_key));
···
167
157
.into_response();
168
158
}
169
159
}
170
-
if !is_migration {
171
-
if let Some(verification_methods) =
172
-
op.get("verificationMethods").and_then(|v| v.as_object())
173
-
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str())
174
-
&& atproto_key != user_did_key
175
-
{
176
-
return (
177
-
StatusCode::BAD_REQUEST,
178
-
Json(json!({
179
-
"error": "InvalidRequest",
180
-
"message": "Incorrect signing key in verificationMethods"
181
-
})),
182
-
)
183
-
.into_response();
184
-
}
160
+
if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object())
161
+
&& let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str())
162
+
&& atproto_key != user_did_key
163
+
{
164
+
return (
165
+
StatusCode::BAD_REQUEST,
166
+
Json(json!({
167
+
"error": "InvalidRequest",
168
+
"message": "Incorrect signing key in verificationMethods"
169
+
})),
170
+
)
171
+
.into_response();
172
+
}
173
+
if !user.handle.is_empty() {
185
174
if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
186
175
let expected_handle = format!("at://{}", user.handle);
187
176
let first_aka = also_known_as.first().and_then(|v| v.as_str());
···
200
189
let plc_client = PlcClient::new(None);
201
190
let operation_clone = input.operation.clone();
202
191
let did_clone = did.clone();
203
-
info!(
204
-
"[MIGRATION] submitPlcOperation: Sending operation to PLC directory for did={}",
205
-
did
206
-
);
207
-
let plc_start = std::time::Instant::now();
208
192
let result: Result<(), CircuitBreakerError<PlcError>> =
209
193
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
210
194
plc_client
···
213
197
})
214
198
.await;
215
199
match result {
216
-
Ok(()) => {
217
-
info!(
218
-
"[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}",
219
-
plc_start.elapsed()
220
-
);
221
-
}
200
+
Ok(()) => {}
222
201
Err(CircuitBreakerError::CircuitOpen(e)) => {
223
-
warn!(
224
-
"[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}",
225
-
e
226
-
);
202
+
warn!("PLC directory circuit breaker open: {}", e);
227
203
return (
228
204
StatusCode::SERVICE_UNAVAILABLE,
229
205
Json(json!({
···
234
210
.into_response();
235
211
}
236
212
Err(CircuitBreakerError::OperationFailed(e)) => {
237
-
error!(
238
-
"[MIGRATION] submitPlcOperation: PLC operation failed: {:?}",
239
-
e
240
-
);
213
+
error!("PLC operation failed: {:?}", e);
241
214
return (
242
215
StatusCode::BAD_GATEWAY,
243
216
Json(json!({
···
248
221
.into_response();
249
222
}
250
223
}
251
-
info!(
252
-
"[MIGRATION] submitPlcOperation: Sequencing identity event for did={}",
253
-
did
254
-
);
255
224
match sqlx::query!(
256
225
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
257
226
did
···
260
229
.await
261
230
{
262
231
Ok(row) => {
263
-
info!(
264
-
"[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}",
265
-
row.seq
266
-
);
267
232
if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
268
233
.execute(&state.db)
269
234
.await
270
235
{
271
-
warn!(
272
-
"[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}",
273
-
e
274
-
);
236
+
warn!("Failed to notify identity event: {:?}", e);
275
237
}
276
238
}
277
239
Err(e) => {
278
-
warn!(
279
-
"[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}",
280
-
e
281
-
);
240
+
warn!("Failed to sequence identity event: {:?}", e);
282
241
}
283
242
}
284
-
info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did);
243
+
let _ = state.cache.delete(&format!("handle:{}", user.handle)).await;
244
+
if state.did_resolver.refresh_did(did).await.is_none() {
245
+
warn!(did = %did, "Failed to refresh DID cache after PLC update");
246
+
}
247
+
info!(did = %did, "PLC operation submitted successfully");
285
248
(StatusCode::OK, Json(json!({}))).into_response()
286
249
}
+12
-12
src/api/validation.rs
+12
-12
src/api/validation.rs
···
35
35
),
36
36
Self::InvalidCharacters => write!(
37
37
f,
38
-
"Handle contains invalid characters. Only alphanumeric, hyphens, and underscores are allowed"
38
+
"Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed"
39
39
),
40
40
Self::StartsWithInvalidChar => {
41
-
write!(f, "Handle cannot start with a hyphen or underscore")
41
+
write!(f, "Handle cannot start with a hyphen")
42
42
}
43
-
Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen or underscore"),
43
+
Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"),
44
44
Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"),
45
45
Self::BannedWord => write!(f, "Inappropriate language in handle"),
46
46
}
···
67
67
}
68
68
69
69
if let Some(first_char) = handle.chars().next()
70
-
&& (first_char == '-' || first_char == '_')
70
+
&& first_char == '-'
71
71
{
72
72
return Err(HandleValidationError::StartsWithInvalidChar);
73
73
}
74
74
75
75
if let Some(last_char) = handle.chars().last()
76
-
&& (last_char == '-' || last_char == '_')
76
+
&& last_char == '-'
77
77
{
78
78
return Err(HandleValidationError::EndsWithInvalidChar);
79
79
}
80
80
81
81
for c in handle.chars() {
82
-
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
82
+
if !c.is_ascii_alphanumeric() && c != '-' {
83
83
return Err(HandleValidationError::InvalidCharacters);
84
84
}
85
85
}
···
151
151
Ok("user-name".to_string())
152
152
);
153
153
assert_eq!(
154
-
validate_short_handle("user_name"),
155
-
Ok("user_name".to_string())
156
-
);
157
-
assert_eq!(
158
154
validate_short_handle("UPPERCASE"),
159
155
Ok("uppercase".to_string())
160
156
);
···
194
190
);
195
191
assert_eq!(
196
192
validate_short_handle("_starts"),
197
-
Err(HandleValidationError::StartsWithInvalidChar)
193
+
Err(HandleValidationError::InvalidCharacters)
198
194
);
199
195
assert_eq!(
200
196
validate_short_handle("ends-"),
···
202
198
);
203
199
assert_eq!(
204
200
validate_short_handle("ends_"),
205
-
Err(HandleValidationError::EndsWithInvalidChar)
201
+
Err(HandleValidationError::InvalidCharacters)
202
+
);
203
+
assert_eq!(
204
+
validate_short_handle("user_name"),
205
+
Err(HandleValidationError::InvalidCharacters)
206
206
);
207
207
assert_eq!(
208
208
validate_short_handle("test@user"),
+8
src/appview/mod.rs
+8
src/appview/mod.rs
···
110
110
Some(resolved)
111
111
}
112
112
113
+
pub async fn refresh_did(&self, did: &str) -> Option<ResolvedService> {
114
+
{
115
+
let mut cache = self.did_cache.write().await;
116
+
cache.remove(did);
117
+
}
118
+
self.resolve_did(did).await
119
+
}
120
+
113
121
async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> {
114
122
let did_doc = if did.starts_with("did:web:") {
115
123
self.resolve_did_web(did).await
+4
src/handle/mod.rs
+4
src/handle/mod.rs
···
93
93
}
94
94
95
95
pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
96
+
if !handle.contains('.') {
97
+
return true;
98
+
}
96
99
let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
97
100
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
98
101
.unwrap_or_else(|_| vec![hostname.to_string()]);
···
115
118
fn test_is_service_domain_handle() {
116
119
assert!(is_service_domain_handle("user.example.com", "example.com"));
117
120
assert!(is_service_domain_handle("example.com", "example.com"));
121
+
assert!(is_service_domain_handle("myhandle", "example.com"));
118
122
assert!(!is_service_domain_handle("user.other.com", "example.com"));
119
123
assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
120
124
}
+12
src/rate_limit.rs
+12
src/rate_limit.rs
···
30
30
pub app_password: Arc<KeyedRateLimiter>,
31
31
pub email_update: Arc<KeyedRateLimiter>,
32
32
pub totp_verify: Arc<KeyedRateLimiter>,
33
+
pub handle_update: Arc<KeyedRateLimiter>,
34
+
pub handle_update_daily: Arc<KeyedRateLimiter>,
33
35
}
34
36
35
37
impl Default for RateLimiters {
···
78
80
Quota::with_period(std::time::Duration::from_secs(60))
79
81
.unwrap()
80
82
.allow_burst(NonZeroU32::new(5).unwrap()),
83
+
)),
84
+
handle_update: Arc::new(RateLimiter::keyed(
85
+
Quota::with_period(std::time::Duration::from_secs(30))
86
+
.unwrap()
87
+
.allow_burst(NonZeroU32::new(10).unwrap()),
88
+
)),
89
+
handle_update_daily: Arc::new(RateLimiter::keyed(
90
+
Quota::with_period(std::time::Duration::from_secs(1728))
91
+
.unwrap()
92
+
.allow_burst(NonZeroU32::new(50).unwrap()),
81
93
)),
82
94
}
83
95
}
+8
src/state.rs
+8
src/state.rs
···
37
37
AppPassword,
38
38
EmailUpdate,
39
39
TotpVerify,
40
+
HandleUpdate,
41
+
HandleUpdateDaily,
40
42
}
41
43
42
44
impl RateLimitKind {
···
54
56
Self::AppPassword => "app_password",
55
57
Self::EmailUpdate => "email_update",
56
58
Self::TotpVerify => "totp_verify",
59
+
Self::HandleUpdate => "handle_update",
60
+
Self::HandleUpdateDaily => "handle_update_daily",
57
61
}
58
62
}
59
63
···
71
75
Self::AppPassword => (10, 60_000),
72
76
Self::EmailUpdate => (5, 3_600_000),
73
77
Self::TotpVerify => (5, 300_000),
78
+
Self::HandleUpdate => (10, 300_000),
79
+
Self::HandleUpdateDaily => (50, 86_400_000),
74
80
}
75
81
}
76
82
}
···
191
197
RateLimitKind::AppPassword => &self.rate_limiters.app_password,
192
198
RateLimitKind::EmailUpdate => &self.rate_limiters.email_update,
193
199
RateLimitKind::TotpVerify => &self.rate_limiters.totp_verify,
200
+
RateLimitKind::HandleUpdate => &self.rate_limiters.handle_update,
201
+
RateLimitKind::HandleUpdateDaily => &self.rate_limiters.handle_update_daily,
194
202
};
195
203
196
204
let ok = limiter.check_key(&client_ip.to_string()).is_ok();
+1
-1
tests/common/mod.rs
+1
-1
tests/common/mod.rs
···
430
430
if attempt > 0 {
431
431
tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await;
432
432
}
433
-
let handle = format!("user_{}", uuid::Uuid::new_v4());
433
+
let handle = format!("user-{}", uuid::Uuid::new_v4());
434
434
let payload = json!({
435
435
"handle": handle,
436
436
"email": format!("{}@example.com", handle),
+7
-7
tests/did_web.rs
+7
-7
tests/did_web.rs
···
11
11
#[tokio::test]
12
12
async fn test_create_self_hosted_did_web() {
13
13
let client = client();
14
-
let handle = format!("selfweb_{}", uuid::Uuid::new_v4());
14
+
let handle = format!("selfweb-{}", uuid::Uuid::new_v4());
15
15
let payload = json!({
16
16
"handle": handle,
17
17
"email": format!("{}@example.com", handle),
···
98
98
let mock_uri = mock_server.uri();
99
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("extweb_{}", uuid::Uuid::new_v4());
101
+
let handle = format!("extweb-{}", uuid::Uuid::new_v4());
102
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
103
104
104
let reserve_res = client
···
180
180
#[tokio::test]
181
181
async fn test_plc_operations_blocked_for_did_web() {
182
182
let client = client();
183
-
let handle = format!("plcblock_{}", uuid::Uuid::new_v4());
183
+
let handle = format!("plcblock-{}", uuid::Uuid::new_v4());
184
184
let payload = json!({
185
185
"handle": handle,
186
186
"email": format!("{}@example.com", handle),
···
245
245
#[tokio::test]
246
246
async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247
247
let client = client();
248
-
let handle = format!("creds_{}", uuid::Uuid::new_v4());
248
+
let handle = format!("creds-{}", uuid::Uuid::new_v4());
249
249
let payload = json!({
250
250
"handle": handle,
251
251
"email": format!("{}@example.com", handle),
···
294
294
#[tokio::test]
295
295
async fn test_did_plc_still_works_with_did_type_param() {
296
296
let client = client();
297
-
let handle = format!("plctype_{}", uuid::Uuid::new_v4());
297
+
let handle = format!("plctype-{}", uuid::Uuid::new_v4());
298
298
let payload = json!({
299
299
"handle": handle,
300
300
"email": format!("{}@example.com", handle),
···
323
323
#[tokio::test]
324
324
async fn test_external_did_web_requires_did_field() {
325
325
let client = client();
326
-
let handle = format!("nodid_{}", uuid::Uuid::new_v4());
326
+
let handle = format!("nodid-{}", uuid::Uuid::new_v4());
327
327
let payload = json!({
328
328
"handle": handle,
329
329
"email": format!("{}@example.com", handle),
···
392
392
mock_addr.replace(":", "%3A"),
393
393
unique_id
394
394
);
395
-
let handle = format!("byod_{}", uuid::Uuid::new_v4());
395
+
let handle = format!("byod-{}", uuid::Uuid::new_v4());
396
396
let pds_endpoint = base_url().await.replace("http://", "https://");
397
397
let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://"));
398
398
+13
-13
tests/email_update.rs
+13
-13
tests/email_update.rs
···
67
67
let client = common::client();
68
68
let base_url = common::base_url().await;
69
69
let pool = get_pool().await;
70
-
let handle = format!("emailup_{}", uuid::Uuid::new_v4());
70
+
let handle = format!("emailup-{}", uuid::Uuid::new_v4());
71
71
let email = format!("{}@example.com", handle);
72
72
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
73
73
let new_email = format!("new_{}@example.com", handle);
···
108
108
async fn test_request_email_update_taken_email() {
109
109
let client = common::client();
110
110
let base_url = common::base_url().await;
111
-
let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4());
111
+
let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4());
112
112
let email1 = format!("{}@example.com", handle1);
113
113
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
114
-
let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4());
114
+
let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4());
115
115
let email2 = format!("{}@example.com", handle2);
116
116
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
117
117
let res = client
···
133
133
async fn test_confirm_email_invalid_token() {
134
134
let client = common::client();
135
135
let base_url = common::base_url().await;
136
-
let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4());
136
+
let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4());
137
137
let email = format!("{}@example.com", handle);
138
138
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
139
139
let new_email = format!("new_{}@example.com", handle);
···
168
168
let client = common::client();
169
169
let base_url = common::base_url().await;
170
170
let pool = get_pool().await;
171
-
let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4());
171
+
let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4());
172
172
let email = format!("{}@example.com", handle);
173
173
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
174
174
let new_email = format!("new_{}@example.com", handle);
···
205
205
async fn test_update_email_requires_token() {
206
206
let client = common::client();
207
207
let base_url = common::base_url().await;
208
-
let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4());
208
+
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
209
209
let email = format!("{}@example.com", handle);
210
210
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
211
211
let new_email = format!("direct_{}@example.com", handle);
···
225
225
async fn test_update_email_same_email_noop() {
226
226
let client = common::client();
227
227
let base_url = common::base_url().await;
228
-
let handle = format!("emailup_same_{}", uuid::Uuid::new_v4());
228
+
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
229
229
let email = format!("{}@example.com", handle);
230
230
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
231
231
let res = client
···
246
246
async fn test_update_email_requires_token_after_pending() {
247
247
let client = common::client();
248
248
let base_url = common::base_url().await;
249
-
let handle = format!("emailup_token_{}", uuid::Uuid::new_v4());
249
+
let handle = format!("emailup-token-{}", uuid::Uuid::new_v4());
250
250
let email = format!("{}@example.com", handle);
251
251
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
252
252
let new_email = format!("pending_{}@example.com", handle);
···
278
278
let client = common::client();
279
279
let base_url = common::base_url().await;
280
280
let pool = get_pool().await;
281
-
let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4());
281
+
let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4());
282
282
let email = format!("{}@example.com", handle);
283
283
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
284
284
let new_email = format!("valid_{}@example.com", handle);
···
316
316
async fn test_update_email_invalid_token() {
317
317
let client = common::client();
318
318
let base_url = common::base_url().await;
319
-
let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4());
319
+
let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4());
320
320
let email = format!("{}@example.com", handle);
321
321
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
322
322
let new_email = format!("badtok_{}@example.com", handle);
···
350
350
async fn test_update_email_already_taken() {
351
351
let client = common::client();
352
352
let base_url = common::base_url().await;
353
-
let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4());
353
+
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
354
354
let email1 = format!("{}@example.com", handle1);
355
355
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
356
-
let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4());
356
+
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
357
357
let email2 = format!("{}@example.com", handle2);
358
358
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
359
359
let res = client
···
394
394
async fn test_update_email_invalid_format() {
395
395
let client = common::client();
396
396
let base_url = common::base_url().await;
397
-
let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4());
397
+
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
398
398
let email = format!("{}@example.com", handle);
399
399
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
400
400
let res = client
+160
-4
tests/identity.rs
+160
-4
tests/identity.rs
···
8
8
#[tokio::test]
9
9
async fn test_resolve_handle_success() {
10
10
let client = client();
11
-
let short_handle = format!("resolvetest_{}", uuid::Uuid::new_v4());
11
+
let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4());
12
12
let payload = json!({
13
13
"handle": short_handle,
14
14
"email": format!("{}@example.com", short_handle),
···
98
98
let mock_uri = mock_server.uri();
99
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("webuser_{}", uuid::Uuid::new_v4());
101
+
let handle = format!("webuser-{}", uuid::Uuid::new_v4());
102
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
103
104
104
let reserve_res = client
···
183
183
#[tokio::test]
184
184
async fn test_create_account_duplicate_handle() {
185
185
let client = client();
186
-
let handle = format!("dupe_{}", uuid::Uuid::new_v4());
186
+
let handle = format!("dupe-{}", uuid::Uuid::new_v4());
187
187
let email = format!("{}@example.com", handle);
188
188
let payload = json!({
189
189
"handle": handle,
···
220
220
let mock_server = MockServer::start().await;
221
221
let mock_uri = mock_server.uri();
222
222
let mock_addr = mock_uri.trim_start_matches("http://");
223
-
let handle = format!("lifecycle_{}", uuid::Uuid::new_v4());
223
+
let handle = format!("lifecycle-{}", uuid::Uuid::new_v4());
224
224
let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle);
225
225
let email = format!("{}@test.com", handle);
226
226
let pds_endpoint = base_url().await.replace("http://", "https://");
···
378
378
let body: Value = res.json().await.expect("Response was not valid JSON");
379
379
assert_eq!(body["error"], "AuthenticationRequired");
380
380
}
381
+
382
+
#[tokio::test]
383
+
async fn test_update_handle_to_same() {
384
+
let client = client();
385
+
let (access_jwt, _did) = create_account_and_login(&client).await;
386
+
let session = client
387
+
.get(format!(
388
+
"{}/xrpc/com.atproto.server.getSession",
389
+
base_url().await
390
+
))
391
+
.bearer_auth(&access_jwt)
392
+
.send()
393
+
.await
394
+
.expect("Failed to get session");
395
+
let session_body: Value = session.json().await.expect("Invalid JSON");
396
+
let current_handle = session_body["handle"].as_str().expect("No handle").to_string();
397
+
let short_handle = current_handle.split('.').next().unwrap_or(¤t_handle);
398
+
let res = client
399
+
.post(format!(
400
+
"{}/xrpc/com.atproto.identity.updateHandle",
401
+
base_url().await
402
+
))
403
+
.bearer_auth(&access_jwt)
404
+
.json(&json!({ "handle": short_handle }))
405
+
.send()
406
+
.await
407
+
.expect("Failed to send request");
408
+
assert_eq!(res.status(), StatusCode::OK);
409
+
}
410
+
411
+
#[tokio::test]
412
+
async fn test_update_handle_no_auth() {
413
+
let client = client();
414
+
let res = client
415
+
.post(format!(
416
+
"{}/xrpc/com.atproto.identity.updateHandle",
417
+
base_url().await
418
+
))
419
+
.json(&json!({ "handle": "newhandle" }))
420
+
.send()
421
+
.await
422
+
.expect("Failed to send request");
423
+
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
424
+
let body: Value = res.json().await.expect("Response was not valid JSON");
425
+
assert_eq!(body["error"], "AuthenticationRequired");
426
+
}
427
+
428
+
#[tokio::test]
429
+
async fn test_update_handle_invalid_characters() {
430
+
let client = client();
431
+
let (access_jwt, _did) = create_account_and_login(&client).await;
432
+
let res = client
433
+
.post(format!(
434
+
"{}/xrpc/com.atproto.identity.updateHandle",
435
+
base_url().await
436
+
))
437
+
.bearer_auth(&access_jwt)
438
+
.json(&json!({ "handle": "invalid@handle!" }))
439
+
.send()
440
+
.await
441
+
.expect("Failed to send request");
442
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
443
+
let body: Value = res.json().await.expect("Response was not valid JSON");
444
+
assert_eq!(body["error"], "InvalidHandle");
445
+
}
446
+
447
+
#[tokio::test]
448
+
async fn test_update_handle_empty() {
449
+
let client = client();
450
+
let (access_jwt, _did) = create_account_and_login(&client).await;
451
+
let res = client
452
+
.post(format!(
453
+
"{}/xrpc/com.atproto.identity.updateHandle",
454
+
base_url().await
455
+
))
456
+
.bearer_auth(&access_jwt)
457
+
.json(&json!({ "handle": "" }))
458
+
.send()
459
+
.await
460
+
.expect("Failed to send request");
461
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
462
+
let body: Value = res.json().await.expect("Response was not valid JSON");
463
+
assert_eq!(body["error"], "InvalidRequest");
464
+
}
465
+
466
+
#[tokio::test]
467
+
async fn test_update_handle_taken() {
468
+
let client = client();
469
+
let (access_jwt1, _did1) = create_account_and_login(&client).await;
470
+
let (access_jwt2, _did2) = create_account_and_login(&client).await;
471
+
let short_handle = format!("taken{}", &uuid::Uuid::new_v4().to_string()[..8]);
472
+
let update1 = client
473
+
.post(format!(
474
+
"{}/xrpc/com.atproto.identity.updateHandle",
475
+
base_url().await
476
+
))
477
+
.bearer_auth(&access_jwt1)
478
+
.json(&json!({ "handle": short_handle }))
479
+
.send()
480
+
.await
481
+
.expect("Failed to update handle");
482
+
assert_eq!(update1.status(), StatusCode::OK);
483
+
let res = client
484
+
.post(format!(
485
+
"{}/xrpc/com.atproto.identity.updateHandle",
486
+
base_url().await
487
+
))
488
+
.bearer_auth(&access_jwt2)
489
+
.json(&json!({ "handle": short_handle }))
490
+
.send()
491
+
.await
492
+
.expect("Failed to send request");
493
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
494
+
let body: Value = res.json().await.expect("Response was not valid JSON");
495
+
assert_eq!(body["error"], "HandleTaken");
496
+
}
497
+
498
+
#[tokio::test]
499
+
async fn test_update_handle_too_short() {
500
+
let client = client();
501
+
let (access_jwt, _did) = create_account_and_login(&client).await;
502
+
let res = client
503
+
.post(format!(
504
+
"{}/xrpc/com.atproto.identity.updateHandle",
505
+
base_url().await
506
+
))
507
+
.bearer_auth(&access_jwt)
508
+
.json(&json!({ "handle": "ab" }))
509
+
.send()
510
+
.await
511
+
.expect("Failed to send request");
512
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
513
+
let body: Value = res.json().await.expect("Response was not valid JSON");
514
+
assert_eq!(body["error"], "InvalidHandle");
515
+
assert!(body["message"].as_str().unwrap().contains("short"));
516
+
}
517
+
518
+
#[tokio::test]
519
+
async fn test_update_handle_too_long() {
520
+
let client = client();
521
+
let (access_jwt, _did) = create_account_and_login(&client).await;
522
+
let res = client
523
+
.post(format!(
524
+
"{}/xrpc/com.atproto.identity.updateHandle",
525
+
base_url().await
526
+
))
527
+
.bearer_auth(&access_jwt)
528
+
.json(&json!({ "handle": "thishandleiswaytoolongforservicedomain" }))
529
+
.send()
530
+
.await
531
+
.expect("Failed to send request");
532
+
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
533
+
let body: Value = res.json().await.expect("Response was not valid JSON");
534
+
assert_eq!(body["error"], "InvalidHandle");
535
+
assert!(body["message"].as_str().unwrap().contains("long"));
536
+
}
+5
-5
tests/password_reset.rs
+5
-5
tests/password_reset.rs
···
19
19
let client = common::client();
20
20
let base_url = common::base_url().await;
21
21
let pool = get_pool().await;
22
-
let handle = format!("pwreset_{}", uuid::Uuid::new_v4());
22
+
let handle = format!("pwreset-{}", uuid::Uuid::new_v4());
23
23
let email = format!("{}@example.com", handle);
24
24
let payload = json!({
25
25
"handle": handle,
···
81
81
let client = common::client();
82
82
let base_url = common::base_url().await;
83
83
let pool = get_pool().await;
84
-
let handle = format!("pwreset2_{}", uuid::Uuid::new_v4());
84
+
let handle = format!("pwreset2-{}", uuid::Uuid::new_v4());
85
85
let email = format!("{}@example.com", handle);
86
86
let old_password = "Oldpass123!";
87
87
let new_password = "Newpass456!";
···
197
197
let client = common::client();
198
198
let base_url = common::base_url().await;
199
199
let pool = get_pool().await;
200
-
let handle = format!("pwreset3_{}", uuid::Uuid::new_v4());
200
+
let handle = format!("pwreset3-{}", uuid::Uuid::new_v4());
201
201
let email = format!("{}@example.com", handle);
202
202
let payload = json!({
203
203
"handle": handle,
···
261
261
let client = common::client();
262
262
let base_url = common::base_url().await;
263
263
let pool = get_pool().await;
264
-
let handle = format!("pwreset4_{}", uuid::Uuid::new_v4());
264
+
let handle = format!("pwreset4-{}", uuid::Uuid::new_v4());
265
265
let email = format!("{}@example.com", handle);
266
266
let payload = json!({
267
267
"handle": handle,
···
351
351
let pool = get_pool().await;
352
352
let client = common::client();
353
353
let base_url = common::base_url().await;
354
-
let handle = format!("pwreset5_{}", uuid::Uuid::new_v4());
354
+
let handle = format!("pwreset5-{}", uuid::Uuid::new_v4());
355
355
let email = format!("{}@example.com", handle);
356
356
let payload = json!({
357
357
"handle": handle,
+4
-4
tests/rate_limit.rs
+4
-4
tests/rate_limit.rs
···
9
9
let client = client();
10
10
let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await);
11
11
let payload = json!({
12
-
"identifier": "nonexistent_user_for_rate_limit_test",
12
+
"identifier": "nonexistent-user-for-rate-limit-test",
13
13
"password": "wrongpassword"
14
14
});
15
15
let mut rate_limited_count = 0;
···
53
53
let mut success_count = 0;
54
54
for i in 0..8 {
55
55
let payload = json!({
56
-
"email": format!("ratelimit_test_{}@example.com", i)
56
+
"email": format!("ratelimit-test_{}@example.com", i)
57
57
});
58
58
let res = client
59
59
.post(&url)
···
91
91
for i in 0..15 {
92
92
let unique_id = uuid::Uuid::new_v4();
93
93
let payload = json!({
94
-
"handle": format!("ratelimit_{}_{}", i, unique_id),
95
-
"email": format!("ratelimit_{}_{}@example.com", i, unique_id),
94
+
"handle": format!("ratelimit-{}_{}", i, unique_id),
95
+
"email": format!("ratelimit-{}_{}@example.com", i, unique_id),
96
96
"password": "Testpass123!"
97
97
});
98
98
let res = client
+1
-1
tests/server.rs
+1
-1
tests/server.rs
···
26
26
async fn test_account_and_session_lifecycle() {
27
27
let client = client();
28
28
let base = base_url().await;
29
-
let handle = format!("user_{}", uuid::Uuid::new_v4());
29
+
let handle = format!("user-{}", uuid::Uuid::new_v4());
30
30
let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
31
31
let create_res = client
32
32
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5
-5
tests/signing_key.rs
+5
-5
tests/signing_key.rs
···
174
174
assert_eq!(res.status(), StatusCode::OK);
175
175
let body: Value = res.json().await.unwrap();
176
176
let signing_key = body["signingKey"].as_str().unwrap();
177
-
let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4());
177
+
let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4());
178
178
let res = client
179
179
.post(format!(
180
180
"{}/xrpc/com.atproto.server.createAccount",
···
212
212
async fn test_create_account_with_invalid_signing_key() {
213
213
let client = common::client();
214
214
let base_url = common::base_url().await;
215
-
let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4());
215
+
let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4());
216
216
let res = client
217
217
.post(format!(
218
218
"{}/xrpc/com.atproto.server.createAccount",
···
248
248
assert_eq!(res.status(), StatusCode::OK);
249
249
let body: Value = res.json().await.unwrap();
250
250
let signing_key = body["signingKey"].as_str().unwrap();
251
-
let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4());
251
+
let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4());
252
252
let res = client
253
253
.post(format!(
254
254
"{}/xrpc/com.atproto.server.createAccount",
···
264
264
.await
265
265
.expect("Failed to create first account");
266
266
assert_eq!(res.status(), StatusCode::OK);
267
-
let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4());
267
+
let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4());
268
268
let res = client
269
269
.post(format!(
270
270
"{}/xrpc/com.atproto.server.createAccount",
···
301
301
assert_eq!(res.status(), StatusCode::OK);
302
302
let body: Value = res.json().await.unwrap();
303
303
let signing_key = body["signingKey"].as_str().unwrap();
304
-
let handle = format!("token_test_user_{}", uuid::Uuid::new_v4());
304
+
let handle = format!("token-test-user-{}", uuid::Uuid::new_v4());
305
305
let res = client
306
306
.post(format!(
307
307
"{}/xrpc/com.atproto.server.createAccount",