+99
-23
src/api/identity/did.rs
+99
-23
src/api/identity/did.rs
···
511
let rotation_keys = if auth_user.did.starts_with("did:web:") {
512
vec![]
513
} else {
514
-
vec![did_key.clone()]
515
};
516
(
517
StatusCode::OK,
···
559
return e;
560
}
561
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)
564
.await
565
{
566
-
Ok(Some(id)) => id,
567
_ => return ApiError::InternalError.into_response(),
568
};
569
-
let new_handle = input.handle.trim();
570
if new_handle.is_empty() {
571
return ApiError::InvalidRequest("handle is required".into()).into_response();
572
}
573
if !new_handle
574
.chars()
575
-
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
576
{
577
return (
578
StatusCode::BAD_REQUEST,
···
582
)
583
.into_response();
584
}
585
-
if crate::moderation::has_explicit_slur(new_handle) {
586
return (
587
StatusCode::BAD_REQUEST,
588
Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})),
···
591
}
592
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
593
let suffix = format!(".{}", hostname);
594
-
let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname);
595
let handle = if is_service_domain {
596
let short_part = if new_handle.ends_with(&suffix) {
597
-
new_handle.strip_suffix(&suffix).unwrap_or(new_handle)
598
} else {
599
-
new_handle
600
};
601
if short_part.contains('.') {
602
return (
603
StatusCode::BAD_REQUEST,
···
608
)
609
.into_response();
610
}
611
-
if new_handle.ends_with(&suffix) {
612
-
new_handle.to_string()
613
-
} else {
614
-
format!("{}.{}", new_handle, hostname)
615
}
616
} else {
617
-
match crate::handle::verify_handle_ownership(new_handle, &did).await {
618
Ok(()) => {}
619
Err(crate::handle::HandleResolutionError::NotFound) => {
620
return (
···
649
.into_response();
650
}
651
}
652
-
new_handle.to_string()
653
};
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
let existing = sqlx::query!(
660
"SELECT id FROM users WHERE handle = $1 AND id != $2",
661
handle,
···
679
.await;
680
match result {
681
Ok(_) => {
682
-
if let Some(old) = old_handle {
683
-
let _ = state.cache.delete(&format!("handle:{}", old)).await;
684
}
685
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
686
if let Err(e) =
···
511
let rotation_keys = if auth_user.did.starts_with("did:web:") {
512
vec![]
513
} else {
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]
522
};
523
(
524
StatusCode::OK,
···
566
return e;
567
}
568
let did = auth_user.did;
569
+
if !state
570
+
.check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did)
571
.await
572
{
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,
597
_ => return ApiError::InternalError.into_response(),
598
};
599
+
let user_id = user_row.id;
600
+
let current_handle = user_row.handle;
601
+
let new_handle = input.handle.trim().to_ascii_lowercase();
602
if new_handle.is_empty() {
603
return ApiError::InvalidRequest("handle is required".into()).into_response();
604
}
605
if !new_handle
606
.chars()
607
+
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
608
{
609
return (
610
StatusCode::BAD_REQUEST,
···
614
)
615
.into_response();
616
}
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) {
634
return (
635
StatusCode::BAD_REQUEST,
636
Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})),
···
639
}
640
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
641
let suffix = format!(".{}", hostname);
642
+
let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname);
643
let handle = if is_service_domain {
644
let short_part = if new_handle.ends_with(&suffix) {
645
+
new_handle.strip_suffix(&suffix).unwrap_or(&new_handle)
646
} else {
647
+
&new_handle
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
+
}
663
if short_part.contains('.') {
664
return (
665
StatusCode::BAD_REQUEST,
···
670
)
671
.into_response();
672
}
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();
686
}
687
+
full_handle
688
} else {
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 {
699
Ok(()) => {}
700
Err(crate::handle::HandleResolutionError::NotFound) => {
701
return (
···
730
.into_response();
731
}
732
}
733
+
new_handle.clone()
734
};
735
let existing = sqlx::query!(
736
"SELECT id FROM users WHERE handle = $1 AND id != $2",
737
handle,
···
755
.await;
756
match result {
757
Ok(_) => {
758
+
if !current_handle.is_empty() {
759
+
let _ = state.cache.delete(&format!("handle:{}", current_handle)).await;
760
}
761
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
762
if let Err(e) =
+28
-65
src/api/identity/plc/submit.rs
+28
-65
src/api/identity/plc/submit.rs
···
23
headers: axum::http::HeaderMap,
24
Json(input): Json<SubmitPlcOperationInput>,
25
) -> Response {
26
-
info!("[MIGRATION] submitPlcOperation called");
27
let bearer = match crate::auth::extract_bearer_token_from_header(
28
headers.get("Authorization").and_then(|h| h.to_str().ok()),
29
) {
30
Some(t) => t,
31
None => {
32
-
info!("[MIGRATION] submitPlcOperation: No bearer token");
33
return ApiError::AuthenticationRequired.into_response();
34
}
35
};
···
37
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
38
Ok(user) => user,
39
Err(e) => {
40
-
info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e);
41
return ApiError::from(e).into_response();
42
}
43
};
44
-
info!(
45
-
"[MIGRATION] submitPlcOperation: Authenticated user did={}",
46
-
auth_user.did
47
-
);
48
if let Err(e) = crate::auth::scope_check::check_identity_scope(
49
auth_user.is_oauth,
50
auth_user.scope.as_deref(),
51
crate::oauth::scopes::IdentityAttr::Wildcard,
52
) {
53
-
info!("[MIGRATION] submitPlcOperation: Scope check failed");
54
return e;
55
}
56
let did = &auth_user.did;
···
67
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
68
let public_url = format!("https://{}", hostname);
69
let user = match sqlx::query!(
70
-
"SELECT id, handle, deactivated_at FROM users WHERE did = $1",
71
did
72
)
73
.fetch_optional(&state.db)
···
82
.into_response();
83
}
84
};
85
-
let is_migration = user.deactivated_at.is_some();
86
let key_row = match sqlx::query!(
87
"SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
88
user.id
···
123
}
124
};
125
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());
130
let has_server_key = rotation_keys
131
.iter()
132
.any(|k| k.as_str() == Some(&server_rotation_key));
···
167
.into_response();
168
}
169
}
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
-
}
185
if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
186
let expected_handle = format!("at://{}", user.handle);
187
let first_aka = also_known_as.first().and_then(|v| v.as_str());
···
200
let plc_client = PlcClient::new(None);
201
let operation_clone = input.operation.clone();
202
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
let result: Result<(), CircuitBreakerError<PlcError>> =
209
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
210
plc_client
···
213
})
214
.await;
215
match result {
216
-
Ok(()) => {
217
-
info!(
218
-
"[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}",
219
-
plc_start.elapsed()
220
-
);
221
-
}
222
Err(CircuitBreakerError::CircuitOpen(e)) => {
223
-
warn!(
224
-
"[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}",
225
-
e
226
-
);
227
return (
228
StatusCode::SERVICE_UNAVAILABLE,
229
Json(json!({
···
234
.into_response();
235
}
236
Err(CircuitBreakerError::OperationFailed(e)) => {
237
-
error!(
238
-
"[MIGRATION] submitPlcOperation: PLC operation failed: {:?}",
239
-
e
240
-
);
241
return (
242
StatusCode::BAD_GATEWAY,
243
Json(json!({
···
248
.into_response();
249
}
250
}
251
-
info!(
252
-
"[MIGRATION] submitPlcOperation: Sequencing identity event for did={}",
253
-
did
254
-
);
255
match sqlx::query!(
256
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
257
did
···
260
.await
261
{
262
Ok(row) => {
263
-
info!(
264
-
"[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}",
265
-
row.seq
266
-
);
267
if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
268
.execute(&state.db)
269
.await
270
{
271
-
warn!(
272
-
"[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}",
273
-
e
274
-
);
275
}
276
}
277
Err(e) => {
278
-
warn!(
279
-
"[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}",
280
-
e
281
-
);
282
}
283
}
284
-
info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did);
285
(StatusCode::OK, Json(json!({}))).into_response()
286
}
···
23
headers: axum::http::HeaderMap,
24
Json(input): Json<SubmitPlcOperationInput>,
25
) -> Response {
26
let bearer = match crate::auth::extract_bearer_token_from_header(
27
headers.get("Authorization").and_then(|h| h.to_str().ok()),
28
) {
29
Some(t) => t,
30
None => {
31
return ApiError::AuthenticationRequired.into_response();
32
}
33
};
···
35
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
36
Ok(user) => user,
37
Err(e) => {
38
return ApiError::from(e).into_response();
39
}
40
};
41
if let Err(e) = crate::auth::scope_check::check_identity_scope(
42
auth_user.is_oauth,
43
auth_user.scope.as_deref(),
44
crate::oauth::scopes::IdentityAttr::Wildcard,
45
) {
46
return e;
47
}
48
let did = &auth_user.did;
···
59
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
60
let public_url = format!("https://{}", hostname);
61
let user = match sqlx::query!(
62
+
"SELECT id, handle FROM users WHERE did = $1",
63
did
64
)
65
.fetch_optional(&state.db)
···
74
.into_response();
75
}
76
};
77
let key_row = match sqlx::query!(
78
"SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
79
user.id
···
114
}
115
};
116
let user_did_key = signing_key_to_did_key(&signing_key);
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()) {
120
let has_server_key = rotation_keys
121
.iter()
122
.any(|k| k.as_str() == Some(&server_rotation_key));
···
157
.into_response();
158
}
159
}
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() {
174
if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
175
let expected_handle = format!("at://{}", user.handle);
176
let first_aka = also_known_as.first().and_then(|v| v.as_str());
···
189
let plc_client = PlcClient::new(None);
190
let operation_clone = input.operation.clone();
191
let did_clone = did.clone();
192
let result: Result<(), CircuitBreakerError<PlcError>> =
193
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
194
plc_client
···
197
})
198
.await;
199
match result {
200
+
Ok(()) => {}
201
Err(CircuitBreakerError::CircuitOpen(e)) => {
202
+
warn!("PLC directory circuit breaker open: {}", e);
203
return (
204
StatusCode::SERVICE_UNAVAILABLE,
205
Json(json!({
···
210
.into_response();
211
}
212
Err(CircuitBreakerError::OperationFailed(e)) => {
213
+
error!("PLC operation failed: {:?}", e);
214
return (
215
StatusCode::BAD_GATEWAY,
216
Json(json!({
···
221
.into_response();
222
}
223
}
224
match sqlx::query!(
225
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
226
did
···
229
.await
230
{
231
Ok(row) => {
232
if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
233
.execute(&state.db)
234
.await
235
{
236
+
warn!("Failed to notify identity event: {:?}", e);
237
}
238
}
239
Err(e) => {
240
+
warn!("Failed to sequence identity event: {:?}", e);
241
}
242
}
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");
248
(StatusCode::OK, Json(json!({}))).into_response()
249
}
+12
-12
src/api/validation.rs
+12
-12
src/api/validation.rs
···
35
),
36
Self::InvalidCharacters => write!(
37
f,
38
-
"Handle contains invalid characters. Only alphanumeric, hyphens, and underscores are allowed"
39
),
40
Self::StartsWithInvalidChar => {
41
-
write!(f, "Handle cannot start with a hyphen or underscore")
42
}
43
-
Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen or underscore"),
44
Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"),
45
Self::BannedWord => write!(f, "Inappropriate language in handle"),
46
}
···
67
}
68
69
if let Some(first_char) = handle.chars().next()
70
-
&& (first_char == '-' || first_char == '_')
71
{
72
return Err(HandleValidationError::StartsWithInvalidChar);
73
}
74
75
if let Some(last_char) = handle.chars().last()
76
-
&& (last_char == '-' || last_char == '_')
77
{
78
return Err(HandleValidationError::EndsWithInvalidChar);
79
}
80
81
for c in handle.chars() {
82
-
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
83
return Err(HandleValidationError::InvalidCharacters);
84
}
85
}
···
151
Ok("user-name".to_string())
152
);
153
assert_eq!(
154
-
validate_short_handle("user_name"),
155
-
Ok("user_name".to_string())
156
-
);
157
-
assert_eq!(
158
validate_short_handle("UPPERCASE"),
159
Ok("uppercase".to_string())
160
);
···
194
);
195
assert_eq!(
196
validate_short_handle("_starts"),
197
-
Err(HandleValidationError::StartsWithInvalidChar)
198
);
199
assert_eq!(
200
validate_short_handle("ends-"),
···
202
);
203
assert_eq!(
204
validate_short_handle("ends_"),
205
-
Err(HandleValidationError::EndsWithInvalidChar)
206
);
207
assert_eq!(
208
validate_short_handle("test@user"),
···
35
),
36
Self::InvalidCharacters => write!(
37
f,
38
+
"Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed"
39
),
40
Self::StartsWithInvalidChar => {
41
+
write!(f, "Handle cannot start with a hyphen")
42
}
43
+
Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"),
44
Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"),
45
Self::BannedWord => write!(f, "Inappropriate language in handle"),
46
}
···
67
}
68
69
if let Some(first_char) = handle.chars().next()
70
+
&& first_char == '-'
71
{
72
return Err(HandleValidationError::StartsWithInvalidChar);
73
}
74
75
if let Some(last_char) = handle.chars().last()
76
+
&& last_char == '-'
77
{
78
return Err(HandleValidationError::EndsWithInvalidChar);
79
}
80
81
for c in handle.chars() {
82
+
if !c.is_ascii_alphanumeric() && c != '-' {
83
return Err(HandleValidationError::InvalidCharacters);
84
}
85
}
···
151
Ok("user-name".to_string())
152
);
153
assert_eq!(
154
validate_short_handle("UPPERCASE"),
155
Ok("uppercase".to_string())
156
);
···
190
);
191
assert_eq!(
192
validate_short_handle("_starts"),
193
+
Err(HandleValidationError::InvalidCharacters)
194
);
195
assert_eq!(
196
validate_short_handle("ends-"),
···
198
);
199
assert_eq!(
200
validate_short_handle("ends_"),
201
+
Err(HandleValidationError::InvalidCharacters)
202
+
);
203
+
assert_eq!(
204
+
validate_short_handle("user_name"),
205
+
Err(HandleValidationError::InvalidCharacters)
206
);
207
assert_eq!(
208
validate_short_handle("test@user"),
+8
src/appview/mod.rs
+8
src/appview/mod.rs
···
110
Some(resolved)
111
}
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
+
121
async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> {
122
let did_doc = if did.starts_with("did:web:") {
123
self.resolve_did_web(did).await
+4
src/handle/mod.rs
+4
src/handle/mod.rs
···
93
}
94
95
pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
96
let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
97
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
98
.unwrap_or_else(|_| vec![hostname.to_string()]);
···
115
fn test_is_service_domain_handle() {
116
assert!(is_service_domain_handle("user.example.com", "example.com"));
117
assert!(is_service_domain_handle("example.com", "example.com"));
118
assert!(!is_service_domain_handle("user.other.com", "example.com"));
119
assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
120
}
···
93
}
94
95
pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
96
+
if !handle.contains('.') {
97
+
return true;
98
+
}
99
let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
100
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
101
.unwrap_or_else(|_| vec![hostname.to_string()]);
···
118
fn test_is_service_domain_handle() {
119
assert!(is_service_domain_handle("user.example.com", "example.com"));
120
assert!(is_service_domain_handle("example.com", "example.com"));
121
+
assert!(is_service_domain_handle("myhandle", "example.com"));
122
assert!(!is_service_domain_handle("user.other.com", "example.com"));
123
assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
124
}
+12
src/rate_limit.rs
+12
src/rate_limit.rs
···
30
pub app_password: Arc<KeyedRateLimiter>,
31
pub email_update: Arc<KeyedRateLimiter>,
32
pub totp_verify: Arc<KeyedRateLimiter>,
33
}
34
35
impl Default for RateLimiters {
···
78
Quota::with_period(std::time::Duration::from_secs(60))
79
.unwrap()
80
.allow_burst(NonZeroU32::new(5).unwrap()),
81
)),
82
}
83
}
···
30
pub app_password: Arc<KeyedRateLimiter>,
31
pub email_update: Arc<KeyedRateLimiter>,
32
pub totp_verify: Arc<KeyedRateLimiter>,
33
+
pub handle_update: Arc<KeyedRateLimiter>,
34
+
pub handle_update_daily: Arc<KeyedRateLimiter>,
35
}
36
37
impl Default for RateLimiters {
···
80
Quota::with_period(std::time::Duration::from_secs(60))
81
.unwrap()
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()),
93
)),
94
}
95
}
+8
src/state.rs
+8
src/state.rs
···
37
AppPassword,
38
EmailUpdate,
39
TotpVerify,
40
}
41
42
impl RateLimitKind {
···
54
Self::AppPassword => "app_password",
55
Self::EmailUpdate => "email_update",
56
Self::TotpVerify => "totp_verify",
57
}
58
}
59
···
71
Self::AppPassword => (10, 60_000),
72
Self::EmailUpdate => (5, 3_600_000),
73
Self::TotpVerify => (5, 300_000),
74
}
75
}
76
}
···
191
RateLimitKind::AppPassword => &self.rate_limiters.app_password,
192
RateLimitKind::EmailUpdate => &self.rate_limiters.email_update,
193
RateLimitKind::TotpVerify => &self.rate_limiters.totp_verify,
194
};
195
196
let ok = limiter.check_key(&client_ip.to_string()).is_ok();
···
37
AppPassword,
38
EmailUpdate,
39
TotpVerify,
40
+
HandleUpdate,
41
+
HandleUpdateDaily,
42
}
43
44
impl RateLimitKind {
···
56
Self::AppPassword => "app_password",
57
Self::EmailUpdate => "email_update",
58
Self::TotpVerify => "totp_verify",
59
+
Self::HandleUpdate => "handle_update",
60
+
Self::HandleUpdateDaily => "handle_update_daily",
61
}
62
}
63
···
75
Self::AppPassword => (10, 60_000),
76
Self::EmailUpdate => (5, 3_600_000),
77
Self::TotpVerify => (5, 300_000),
78
+
Self::HandleUpdate => (10, 300_000),
79
+
Self::HandleUpdateDaily => (50, 86_400_000),
80
}
81
}
82
}
···
197
RateLimitKind::AppPassword => &self.rate_limiters.app_password,
198
RateLimitKind::EmailUpdate => &self.rate_limiters.email_update,
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,
202
};
203
204
let ok = limiter.check_key(&client_ip.to_string()).is_ok();
+1
-1
tests/common/mod.rs
+1
-1
tests/common/mod.rs
+7
-7
tests/did_web.rs
+7
-7
tests/did_web.rs
···
11
#[tokio::test]
12
async fn test_create_self_hosted_did_web() {
13
let client = client();
14
-
let handle = format!("selfweb_{}", uuid::Uuid::new_v4());
15
let payload = json!({
16
"handle": handle,
17
"email": format!("{}@example.com", handle),
···
98
let mock_uri = mock_server.uri();
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("extweb_{}", uuid::Uuid::new_v4());
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
104
let reserve_res = client
···
180
#[tokio::test]
181
async fn test_plc_operations_blocked_for_did_web() {
182
let client = client();
183
-
let handle = format!("plcblock_{}", uuid::Uuid::new_v4());
184
let payload = json!({
185
"handle": handle,
186
"email": format!("{}@example.com", handle),
···
245
#[tokio::test]
246
async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247
let client = client();
248
-
let handle = format!("creds_{}", uuid::Uuid::new_v4());
249
let payload = json!({
250
"handle": handle,
251
"email": format!("{}@example.com", handle),
···
294
#[tokio::test]
295
async fn test_did_plc_still_works_with_did_type_param() {
296
let client = client();
297
-
let handle = format!("plctype_{}", uuid::Uuid::new_v4());
298
let payload = json!({
299
"handle": handle,
300
"email": format!("{}@example.com", handle),
···
323
#[tokio::test]
324
async fn test_external_did_web_requires_did_field() {
325
let client = client();
326
-
let handle = format!("nodid_{}", uuid::Uuid::new_v4());
327
let payload = json!({
328
"handle": handle,
329
"email": format!("{}@example.com", handle),
···
392
mock_addr.replace(":", "%3A"),
393
unique_id
394
);
395
-
let handle = format!("byod_{}", uuid::Uuid::new_v4());
396
let pds_endpoint = base_url().await.replace("http://", "https://");
397
let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://"));
398
···
11
#[tokio::test]
12
async fn test_create_self_hosted_did_web() {
13
let client = client();
14
+
let handle = format!("selfweb-{}", uuid::Uuid::new_v4());
15
let payload = json!({
16
"handle": handle,
17
"email": format!("{}@example.com", handle),
···
98
let mock_uri = mock_server.uri();
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
+
let handle = format!("extweb-{}", uuid::Uuid::new_v4());
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
104
let reserve_res = client
···
180
#[tokio::test]
181
async fn test_plc_operations_blocked_for_did_web() {
182
let client = client();
183
+
let handle = format!("plcblock-{}", uuid::Uuid::new_v4());
184
let payload = json!({
185
"handle": handle,
186
"email": format!("{}@example.com", handle),
···
245
#[tokio::test]
246
async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247
let client = client();
248
+
let handle = format!("creds-{}", uuid::Uuid::new_v4());
249
let payload = json!({
250
"handle": handle,
251
"email": format!("{}@example.com", handle),
···
294
#[tokio::test]
295
async fn test_did_plc_still_works_with_did_type_param() {
296
let client = client();
297
+
let handle = format!("plctype-{}", uuid::Uuid::new_v4());
298
let payload = json!({
299
"handle": handle,
300
"email": format!("{}@example.com", handle),
···
323
#[tokio::test]
324
async fn test_external_did_web_requires_did_field() {
325
let client = client();
326
+
let handle = format!("nodid-{}", uuid::Uuid::new_v4());
327
let payload = json!({
328
"handle": handle,
329
"email": format!("{}@example.com", handle),
···
392
mock_addr.replace(":", "%3A"),
393
unique_id
394
);
395
+
let handle = format!("byod-{}", uuid::Uuid::new_v4());
396
let pds_endpoint = base_url().await.replace("http://", "https://");
397
let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://"));
398
+13
-13
tests/email_update.rs
+13
-13
tests/email_update.rs
···
67
let client = common::client();
68
let base_url = common::base_url().await;
69
let pool = get_pool().await;
70
-
let handle = format!("emailup_{}", uuid::Uuid::new_v4());
71
let email = format!("{}@example.com", handle);
72
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
73
let new_email = format!("new_{}@example.com", handle);
···
108
async fn test_request_email_update_taken_email() {
109
let client = common::client();
110
let base_url = common::base_url().await;
111
-
let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4());
112
let email1 = format!("{}@example.com", handle1);
113
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
114
-
let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4());
115
let email2 = format!("{}@example.com", handle2);
116
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
117
let res = client
···
133
async fn test_confirm_email_invalid_token() {
134
let client = common::client();
135
let base_url = common::base_url().await;
136
-
let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4());
137
let email = format!("{}@example.com", handle);
138
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
139
let new_email = format!("new_{}@example.com", handle);
···
168
let client = common::client();
169
let base_url = common::base_url().await;
170
let pool = get_pool().await;
171
-
let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4());
172
let email = format!("{}@example.com", handle);
173
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
174
let new_email = format!("new_{}@example.com", handle);
···
205
async fn test_update_email_requires_token() {
206
let client = common::client();
207
let base_url = common::base_url().await;
208
-
let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4());
209
let email = format!("{}@example.com", handle);
210
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
211
let new_email = format!("direct_{}@example.com", handle);
···
225
async fn test_update_email_same_email_noop() {
226
let client = common::client();
227
let base_url = common::base_url().await;
228
-
let handle = format!("emailup_same_{}", uuid::Uuid::new_v4());
229
let email = format!("{}@example.com", handle);
230
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
231
let res = client
···
246
async fn test_update_email_requires_token_after_pending() {
247
let client = common::client();
248
let base_url = common::base_url().await;
249
-
let handle = format!("emailup_token_{}", uuid::Uuid::new_v4());
250
let email = format!("{}@example.com", handle);
251
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
252
let new_email = format!("pending_{}@example.com", handle);
···
278
let client = common::client();
279
let base_url = common::base_url().await;
280
let pool = get_pool().await;
281
-
let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4());
282
let email = format!("{}@example.com", handle);
283
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
284
let new_email = format!("valid_{}@example.com", handle);
···
316
async fn test_update_email_invalid_token() {
317
let client = common::client();
318
let base_url = common::base_url().await;
319
-
let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4());
320
let email = format!("{}@example.com", handle);
321
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
322
let new_email = format!("badtok_{}@example.com", handle);
···
350
async fn test_update_email_already_taken() {
351
let client = common::client();
352
let base_url = common::base_url().await;
353
-
let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4());
354
let email1 = format!("{}@example.com", handle1);
355
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
356
-
let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4());
357
let email2 = format!("{}@example.com", handle2);
358
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
359
let res = client
···
394
async fn test_update_email_invalid_format() {
395
let client = common::client();
396
let base_url = common::base_url().await;
397
-
let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4());
398
let email = format!("{}@example.com", handle);
399
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
400
let res = client
···
67
let client = common::client();
68
let base_url = common::base_url().await;
69
let pool = get_pool().await;
70
+
let handle = format!("emailup-{}", uuid::Uuid::new_v4());
71
let email = format!("{}@example.com", handle);
72
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
73
let new_email = format!("new_{}@example.com", handle);
···
108
async fn test_request_email_update_taken_email() {
109
let client = common::client();
110
let base_url = common::base_url().await;
111
+
let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4());
112
let email1 = format!("{}@example.com", handle1);
113
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
114
+
let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4());
115
let email2 = format!("{}@example.com", handle2);
116
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
117
let res = client
···
133
async fn test_confirm_email_invalid_token() {
134
let client = common::client();
135
let base_url = common::base_url().await;
136
+
let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4());
137
let email = format!("{}@example.com", handle);
138
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
139
let new_email = format!("new_{}@example.com", handle);
···
168
let client = common::client();
169
let base_url = common::base_url().await;
170
let pool = get_pool().await;
171
+
let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4());
172
let email = format!("{}@example.com", handle);
173
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
174
let new_email = format!("new_{}@example.com", handle);
···
205
async fn test_update_email_requires_token() {
206
let client = common::client();
207
let base_url = common::base_url().await;
208
+
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
209
let email = format!("{}@example.com", handle);
210
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
211
let new_email = format!("direct_{}@example.com", handle);
···
225
async fn test_update_email_same_email_noop() {
226
let client = common::client();
227
let base_url = common::base_url().await;
228
+
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
229
let email = format!("{}@example.com", handle);
230
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
231
let res = client
···
246
async fn test_update_email_requires_token_after_pending() {
247
let client = common::client();
248
let base_url = common::base_url().await;
249
+
let handle = format!("emailup-token-{}", uuid::Uuid::new_v4());
250
let email = format!("{}@example.com", handle);
251
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
252
let new_email = format!("pending_{}@example.com", handle);
···
278
let client = common::client();
279
let base_url = common::base_url().await;
280
let pool = get_pool().await;
281
+
let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4());
282
let email = format!("{}@example.com", handle);
283
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
284
let new_email = format!("valid_{}@example.com", handle);
···
316
async fn test_update_email_invalid_token() {
317
let client = common::client();
318
let base_url = common::base_url().await;
319
+
let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4());
320
let email = format!("{}@example.com", handle);
321
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
322
let new_email = format!("badtok_{}@example.com", handle);
···
350
async fn test_update_email_already_taken() {
351
let client = common::client();
352
let base_url = common::base_url().await;
353
+
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
354
let email1 = format!("{}@example.com", handle1);
355
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
356
+
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
357
let email2 = format!("{}@example.com", handle2);
358
let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
359
let res = client
···
394
async fn test_update_email_invalid_format() {
395
let client = common::client();
396
let base_url = common::base_url().await;
397
+
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
398
let email = format!("{}@example.com", handle);
399
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
400
let res = client
+160
-4
tests/identity.rs
+160
-4
tests/identity.rs
···
8
#[tokio::test]
9
async fn test_resolve_handle_success() {
10
let client = client();
11
-
let short_handle = format!("resolvetest_{}", uuid::Uuid::new_v4());
12
let payload = json!({
13
"handle": short_handle,
14
"email": format!("{}@example.com", short_handle),
···
98
let mock_uri = mock_server.uri();
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("webuser_{}", uuid::Uuid::new_v4());
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
104
let reserve_res = client
···
183
#[tokio::test]
184
async fn test_create_account_duplicate_handle() {
185
let client = client();
186
-
let handle = format!("dupe_{}", uuid::Uuid::new_v4());
187
let email = format!("{}@example.com", handle);
188
let payload = json!({
189
"handle": handle,
···
220
let mock_server = MockServer::start().await;
221
let mock_uri = mock_server.uri();
222
let mock_addr = mock_uri.trim_start_matches("http://");
223
-
let handle = format!("lifecycle_{}", uuid::Uuid::new_v4());
224
let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle);
225
let email = format!("{}@test.com", handle);
226
let pds_endpoint = base_url().await.replace("http://", "https://");
···
378
let body: Value = res.json().await.expect("Response was not valid JSON");
379
assert_eq!(body["error"], "AuthenticationRequired");
380
}
···
8
#[tokio::test]
9
async fn test_resolve_handle_success() {
10
let client = client();
11
+
let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4());
12
let payload = json!({
13
"handle": short_handle,
14
"email": format!("{}@example.com", short_handle),
···
98
let mock_uri = mock_server.uri();
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
+
let handle = format!("webuser-{}", uuid::Uuid::new_v4());
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
104
let reserve_res = client
···
183
#[tokio::test]
184
async fn test_create_account_duplicate_handle() {
185
let client = client();
186
+
let handle = format!("dupe-{}", uuid::Uuid::new_v4());
187
let email = format!("{}@example.com", handle);
188
let payload = json!({
189
"handle": handle,
···
220
let mock_server = MockServer::start().await;
221
let mock_uri = mock_server.uri();
222
let mock_addr = mock_uri.trim_start_matches("http://");
223
+
let handle = format!("lifecycle-{}", uuid::Uuid::new_v4());
224
let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle);
225
let email = format!("{}@test.com", handle);
226
let pds_endpoint = base_url().await.replace("http://", "https://");
···
378
let body: Value = res.json().await.expect("Response was not valid JSON");
379
assert_eq!(body["error"], "AuthenticationRequired");
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
let client = common::client();
20
let base_url = common::base_url().await;
21
let pool = get_pool().await;
22
-
let handle = format!("pwreset_{}", uuid::Uuid::new_v4());
23
let email = format!("{}@example.com", handle);
24
let payload = json!({
25
"handle": handle,
···
81
let client = common::client();
82
let base_url = common::base_url().await;
83
let pool = get_pool().await;
84
-
let handle = format!("pwreset2_{}", uuid::Uuid::new_v4());
85
let email = format!("{}@example.com", handle);
86
let old_password = "Oldpass123!";
87
let new_password = "Newpass456!";
···
197
let client = common::client();
198
let base_url = common::base_url().await;
199
let pool = get_pool().await;
200
-
let handle = format!("pwreset3_{}", uuid::Uuid::new_v4());
201
let email = format!("{}@example.com", handle);
202
let payload = json!({
203
"handle": handle,
···
261
let client = common::client();
262
let base_url = common::base_url().await;
263
let pool = get_pool().await;
264
-
let handle = format!("pwreset4_{}", uuid::Uuid::new_v4());
265
let email = format!("{}@example.com", handle);
266
let payload = json!({
267
"handle": handle,
···
351
let pool = get_pool().await;
352
let client = common::client();
353
let base_url = common::base_url().await;
354
-
let handle = format!("pwreset5_{}", uuid::Uuid::new_v4());
355
let email = format!("{}@example.com", handle);
356
let payload = json!({
357
"handle": handle,
···
19
let client = common::client();
20
let base_url = common::base_url().await;
21
let pool = get_pool().await;
22
+
let handle = format!("pwreset-{}", uuid::Uuid::new_v4());
23
let email = format!("{}@example.com", handle);
24
let payload = json!({
25
"handle": handle,
···
81
let client = common::client();
82
let base_url = common::base_url().await;
83
let pool = get_pool().await;
84
+
let handle = format!("pwreset2-{}", uuid::Uuid::new_v4());
85
let email = format!("{}@example.com", handle);
86
let old_password = "Oldpass123!";
87
let new_password = "Newpass456!";
···
197
let client = common::client();
198
let base_url = common::base_url().await;
199
let pool = get_pool().await;
200
+
let handle = format!("pwreset3-{}", uuid::Uuid::new_v4());
201
let email = format!("{}@example.com", handle);
202
let payload = json!({
203
"handle": handle,
···
261
let client = common::client();
262
let base_url = common::base_url().await;
263
let pool = get_pool().await;
264
+
let handle = format!("pwreset4-{}", uuid::Uuid::new_v4());
265
let email = format!("{}@example.com", handle);
266
let payload = json!({
267
"handle": handle,
···
351
let pool = get_pool().await;
352
let client = common::client();
353
let base_url = common::base_url().await;
354
+
let handle = format!("pwreset5-{}", uuid::Uuid::new_v4());
355
let email = format!("{}@example.com", handle);
356
let payload = json!({
357
"handle": handle,
+4
-4
tests/rate_limit.rs
+4
-4
tests/rate_limit.rs
···
9
let client = client();
10
let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await);
11
let payload = json!({
12
-
"identifier": "nonexistent_user_for_rate_limit_test",
13
"password": "wrongpassword"
14
});
15
let mut rate_limited_count = 0;
···
53
let mut success_count = 0;
54
for i in 0..8 {
55
let payload = json!({
56
-
"email": format!("ratelimit_test_{}@example.com", i)
57
});
58
let res = client
59
.post(&url)
···
91
for i in 0..15 {
92
let unique_id = uuid::Uuid::new_v4();
93
let payload = json!({
94
-
"handle": format!("ratelimit_{}_{}", i, unique_id),
95
-
"email": format!("ratelimit_{}_{}@example.com", i, unique_id),
96
"password": "Testpass123!"
97
});
98
let res = client
···
9
let client = client();
10
let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await);
11
let payload = json!({
12
+
"identifier": "nonexistent-user-for-rate-limit-test",
13
"password": "wrongpassword"
14
});
15
let mut rate_limited_count = 0;
···
53
let mut success_count = 0;
54
for i in 0..8 {
55
let payload = json!({
56
+
"email": format!("ratelimit-test_{}@example.com", i)
57
});
58
let res = client
59
.post(&url)
···
91
for i in 0..15 {
92
let unique_id = uuid::Uuid::new_v4();
93
let payload = json!({
94
+
"handle": format!("ratelimit-{}_{}", i, unique_id),
95
+
"email": format!("ratelimit-{}_{}@example.com", i, unique_id),
96
"password": "Testpass123!"
97
});
98
let res = client
+1
-1
tests/server.rs
+1
-1
tests/server.rs
···
26
async fn test_account_and_session_lifecycle() {
27
let client = client();
28
let base = base_url().await;
29
-
let handle = format!("user_{}", uuid::Uuid::new_v4());
30
let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
31
let create_res = client
32
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
···
26
async fn test_account_and_session_lifecycle() {
27
let client = client();
28
let base = base_url().await;
29
+
let handle = format!("user-{}", uuid::Uuid::new_v4());
30
let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
31
let create_res = client
32
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5
-5
tests/signing_key.rs
+5
-5
tests/signing_key.rs
···
174
assert_eq!(res.status(), StatusCode::OK);
175
let body: Value = res.json().await.unwrap();
176
let signing_key = body["signingKey"].as_str().unwrap();
177
-
let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4());
178
let res = client
179
.post(format!(
180
"{}/xrpc/com.atproto.server.createAccount",
···
212
async fn test_create_account_with_invalid_signing_key() {
213
let client = common::client();
214
let base_url = common::base_url().await;
215
-
let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4());
216
let res = client
217
.post(format!(
218
"{}/xrpc/com.atproto.server.createAccount",
···
248
assert_eq!(res.status(), StatusCode::OK);
249
let body: Value = res.json().await.unwrap();
250
let signing_key = body["signingKey"].as_str().unwrap();
251
-
let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4());
252
let res = client
253
.post(format!(
254
"{}/xrpc/com.atproto.server.createAccount",
···
264
.await
265
.expect("Failed to create first account");
266
assert_eq!(res.status(), StatusCode::OK);
267
-
let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4());
268
let res = client
269
.post(format!(
270
"{}/xrpc/com.atproto.server.createAccount",
···
301
assert_eq!(res.status(), StatusCode::OK);
302
let body: Value = res.json().await.unwrap();
303
let signing_key = body["signingKey"].as_str().unwrap();
304
-
let handle = format!("token_test_user_{}", uuid::Uuid::new_v4());
305
let res = client
306
.post(format!(
307
"{}/xrpc/com.atproto.server.createAccount",
···
174
assert_eq!(res.status(), StatusCode::OK);
175
let body: Value = res.json().await.unwrap();
176
let signing_key = body["signingKey"].as_str().unwrap();
177
+
let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4());
178
let res = client
179
.post(format!(
180
"{}/xrpc/com.atproto.server.createAccount",
···
212
async fn test_create_account_with_invalid_signing_key() {
213
let client = common::client();
214
let base_url = common::base_url().await;
215
+
let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4());
216
let res = client
217
.post(format!(
218
"{}/xrpc/com.atproto.server.createAccount",
···
248
assert_eq!(res.status(), StatusCode::OK);
249
let body: Value = res.json().await.unwrap();
250
let signing_key = body["signingKey"].as_str().unwrap();
251
+
let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4());
252
let res = client
253
.post(format!(
254
"{}/xrpc/com.atproto.server.createAccount",
···
264
.await
265
.expect("Failed to create first account");
266
assert_eq!(res.status(), StatusCode::OK);
267
+
let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4());
268
let res = client
269
.post(format!(
270
"{}/xrpc/com.atproto.server.createAccount",
···
301
assert_eq!(res.status(), StatusCode::OK);
302
let body: Value = res.json().await.unwrap();
303
let signing_key = body["signingKey"].as_str().unwrap();
304
+
let handle = format!("token-test-user-{}", uuid::Uuid::new_v4());
305
let res = client
306
.post(format!(
307
"{}/xrpc/com.atproto.server.createAccount",