this repo has no description
1use super::did::verify_did_web;
2use crate::plc::{create_genesis_operation, signing_key_to_did_key, PlcClient};
3use crate::state::{AppState, RateLimitKind};
4use axum::{
5 Json,
6 extract::State,
7 http::{HeaderMap, StatusCode},
8 response::{IntoResponse, Response},
9};
10use bcrypt::{DEFAULT_COST, hash};
11use jacquard::types::{did::Did, integer::LimitedU32, string::Tid};
12use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
13use k256::{ecdsa::SigningKey, SecretKey};
14use rand::rngs::OsRng;
15use serde::{Deserialize, Serialize};
16use serde_json::json;
17use std::sync::Arc;
18use tracing::{error, info, warn};
19
20fn extract_client_ip(headers: &HeaderMap) -> String {
21 if let Some(forwarded) = headers.get("x-forwarded-for") {
22 if let Ok(value) = forwarded.to_str() {
23 if let Some(first_ip) = value.split(',').next() {
24 return first_ip.trim().to_string();
25 }
26 }
27 }
28 if let Some(real_ip) = headers.get("x-real-ip") {
29 if let Ok(value) = real_ip.to_str() {
30 return value.trim().to_string();
31 }
32 }
33 "unknown".to_string()
34}
35
36#[derive(Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct CreateAccountInput {
39 pub handle: String,
40 pub email: Option<String>,
41 pub password: String,
42 pub invite_code: Option<String>,
43 pub did: Option<String>,
44 pub signing_key: Option<String>,
45 pub verification_channel: Option<String>,
46 pub discord_id: Option<String>,
47 pub telegram_username: Option<String>,
48 pub signal_number: Option<String>,
49}
50
51#[derive(Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct CreateAccountOutput {
54 pub handle: String,
55 pub did: String,
56 pub verification_required: bool,
57 pub verification_channel: String,
58}
59
60pub async fn create_account(
61 State(state): State<AppState>,
62 headers: HeaderMap,
63 Json(input): Json<CreateAccountInput>,
64) -> Response {
65 info!("create_account called");
66 let client_ip = extract_client_ip(&headers);
67 if !state.check_rate_limit(RateLimitKind::AccountCreation, &client_ip).await {
68 warn!(ip = %client_ip, "Account creation rate limit exceeded");
69 return (
70 StatusCode::TOO_MANY_REQUESTS,
71 Json(json!({
72 "error": "RateLimitExceeded",
73 "message": "Too many account creation attempts. Please try again later."
74 })),
75 )
76 .into_response();
77 }
78 if input.handle.contains('!') || input.handle.contains('@') {
79 return (
80 StatusCode::BAD_REQUEST,
81 Json(
82 json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
83 ),
84 )
85 .into_response();
86 }
87 let email: Option<String> = input.email.as_ref()
88 .map(|e| e.trim().to_string())
89 .filter(|e| !e.is_empty());
90 if let Some(ref email) = email {
91 if !crate::api::validation::is_valid_email(email) {
92 return (
93 StatusCode::BAD_REQUEST,
94 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
95 )
96 .into_response();
97 }
98 }
99 let verification_channel = input.verification_channel.as_deref().unwrap_or("email");
100 let valid_channels = ["email", "discord", "telegram", "signal"];
101 if !valid_channels.contains(&verification_channel) {
102 return (
103 StatusCode::BAD_REQUEST,
104 Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel. Must be one of: email, discord, telegram, signal"})),
105 )
106 .into_response();
107 }
108 let verification_recipient = match verification_channel {
109 "email" => match &input.email {
110 Some(email) if !email.trim().is_empty() => email.trim().to_string(),
111 _ => return (
112 StatusCode::BAD_REQUEST,
113 Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})),
114 ).into_response(),
115 },
116 "discord" => match &input.discord_id {
117 Some(id) if !id.trim().is_empty() => id.trim().to_string(),
118 _ => return (
119 StatusCode::BAD_REQUEST,
120 Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})),
121 ).into_response(),
122 },
123 "telegram" => match &input.telegram_username {
124 Some(username) if !username.trim().is_empty() => username.trim().to_string(),
125 _ => return (
126 StatusCode::BAD_REQUEST,
127 Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})),
128 ).into_response(),
129 },
130 "signal" => match &input.signal_number {
131 Some(number) if !number.trim().is_empty() => number.trim().to_string(),
132 _ => return (
133 StatusCode::BAD_REQUEST,
134 Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})),
135 ).into_response(),
136 },
137 _ => return (
138 StatusCode::BAD_REQUEST,
139 Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})),
140 ).into_response(),
141 };
142 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
143 let pds_endpoint = format!("https://{}", hostname);
144 let suffix = format!(".{}", hostname);
145 let short_handle = if input.handle.ends_with(&suffix) {
146 input.handle.strip_suffix(&suffix).unwrap_or(&input.handle)
147 } else {
148 &input.handle
149 };
150 let full_handle = format!("{}.{}", short_handle, hostname);
151 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
152 if let Some(signing_key_did) = &input.signing_key {
153 let reserved = sqlx::query!(
154 r#"
155 SELECT id, private_key_bytes
156 FROM reserved_signing_keys
157 WHERE public_key_did_key = $1
158 AND used_at IS NULL
159 AND expires_at > NOW()
160 FOR UPDATE
161 "#,
162 signing_key_did
163 )
164 .fetch_optional(&state.db)
165 .await;
166 match reserved {
167 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)),
168 Ok(None) => {
169 return (
170 StatusCode::BAD_REQUEST,
171 Json(json!({
172 "error": "InvalidSigningKey",
173 "message": "Signing key not found, already used, or expired"
174 })),
175 )
176 .into_response();
177 }
178 Err(e) => {
179 error!("Error looking up reserved signing key: {:?}", e);
180 return (
181 StatusCode::INTERNAL_SERVER_ERROR,
182 Json(json!({"error": "InternalError"})),
183 )
184 .into_response();
185 }
186 }
187 } else {
188 let secret_key = SecretKey::random(&mut OsRng);
189 (secret_key.to_bytes().to_vec(), None)
190 };
191 let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
192 Ok(k) => k,
193 Err(e) => {
194 error!("Error creating signing key: {:?}", e);
195 return (
196 StatusCode::INTERNAL_SERVER_ERROR,
197 Json(json!({"error": "InternalError"})),
198 )
199 .into_response();
200 }
201 };
202 let did = if let Some(d) = &input.did {
203 if d.trim().is_empty() {
204 let rotation_key = std::env::var("PLC_ROTATION_KEY")
205 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
206 let genesis_result = match create_genesis_operation(
207 &signing_key,
208 &rotation_key,
209 &full_handle,
210 &pds_endpoint,
211 ) {
212 Ok(r) => r,
213 Err(e) => {
214 error!("Error creating PLC genesis operation: {:?}", e);
215 return (
216 StatusCode::INTERNAL_SERVER_ERROR,
217 Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})),
218 )
219 .into_response();
220 }
221 };
222 let plc_client = PlcClient::new(None);
223 if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
224 error!("Failed to submit PLC genesis operation: {:?}", e);
225 return (
226 StatusCode::BAD_GATEWAY,
227 Json(json!({
228 "error": "UpstreamError",
229 "message": format!("Failed to register DID with PLC directory: {}", e)
230 })),
231 )
232 .into_response();
233 }
234 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
235 genesis_result.did
236 } else if d.starts_with("did:web:") {
237 if let Err(e) = verify_did_web(d, &hostname, &input.handle).await {
238 return (
239 StatusCode::BAD_REQUEST,
240 Json(json!({"error": "InvalidDid", "message": e})),
241 )
242 .into_response();
243 }
244 d.clone()
245 } else {
246 return (
247 StatusCode::BAD_REQUEST,
248 Json(json!({"error": "InvalidDid", "message": "Only did:web DIDs can be provided; leave empty for did:plc"})),
249 )
250 .into_response();
251 }
252 } else {
253 let rotation_key = std::env::var("PLC_ROTATION_KEY")
254 .unwrap_or_else(|_| signing_key_to_did_key(&signing_key));
255 let genesis_result = match create_genesis_operation(
256 &signing_key,
257 &rotation_key,
258 &full_handle,
259 &pds_endpoint,
260 ) {
261 Ok(r) => r,
262 Err(e) => {
263 error!("Error creating PLC genesis operation: {:?}", e);
264 return (
265 StatusCode::INTERNAL_SERVER_ERROR,
266 Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})),
267 )
268 .into_response();
269 }
270 };
271 let plc_client = PlcClient::new(None);
272 if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
273 error!("Failed to submit PLC genesis operation: {:?}", e);
274 return (
275 StatusCode::BAD_GATEWAY,
276 Json(json!({
277 "error": "UpstreamError",
278 "message": format!("Failed to register DID with PLC directory: {}", e)
279 })),
280 )
281 .into_response();
282 }
283 info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
284 genesis_result.did
285 };
286 let mut tx = match state.db.begin().await {
287 Ok(tx) => tx,
288 Err(e) => {
289 error!("Error starting transaction: {:?}", e);
290 return (
291 StatusCode::INTERNAL_SERVER_ERROR,
292 Json(json!({"error": "InternalError"})),
293 )
294 .into_response();
295 }
296 };
297 let exists_query = sqlx::query!("SELECT 1 as one FROM users WHERE handle = $1", short_handle)
298 .fetch_optional(&mut *tx)
299 .await;
300 match exists_query {
301 Ok(Some(_)) => {
302 return (
303 StatusCode::BAD_REQUEST,
304 Json(json!({"error": "HandleTaken", "message": "Handle already taken"})),
305 )
306 .into_response();
307 }
308 Err(e) => {
309 error!("Error checking handle: {:?}", e);
310 return (
311 StatusCode::INTERNAL_SERVER_ERROR,
312 Json(json!({"error": "InternalError"})),
313 )
314 .into_response();
315 }
316 Ok(None) => {}
317 }
318 if let Some(code) = &input.invite_code {
319 let invite_query =
320 sqlx::query!("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", code)
321 .fetch_optional(&mut *tx)
322 .await;
323 match invite_query {
324 Ok(Some(row)) => {
325 if row.available_uses <= 0 {
326 return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
327 }
328 let update_invite = sqlx::query!(
329 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
330 code
331 )
332 .execute(&mut *tx)
333 .await;
334 if let Err(e) = update_invite {
335 error!("Error updating invite code: {:?}", e);
336 return (
337 StatusCode::INTERNAL_SERVER_ERROR,
338 Json(json!({"error": "InternalError"})),
339 )
340 .into_response();
341 }
342 }
343 Ok(None) => {
344 return (
345 StatusCode::BAD_REQUEST,
346 Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})),
347 )
348 .into_response();
349 }
350 Err(e) => {
351 error!("Error checking invite code: {:?}", e);
352 return (
353 StatusCode::INTERNAL_SERVER_ERROR,
354 Json(json!({"error": "InternalError"})),
355 )
356 .into_response();
357 }
358 }
359 }
360 let password_hash = match hash(&input.password, DEFAULT_COST) {
361 Ok(h) => h,
362 Err(e) => {
363 error!("Error hashing password: {:?}", e);
364 return (
365 StatusCode::INTERNAL_SERVER_ERROR,
366 Json(json!({"error": "InternalError"})),
367 )
368 .into_response();
369 }
370 };
371 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000);
372 let code_expires_at = chrono::Utc::now() + chrono::Duration::minutes(30);
373 let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
374 r#"INSERT INTO users (
375 handle, email, did, password_hash,
376 email_confirmation_code, email_confirmation_code_expires_at,
377 preferred_notification_channel,
378 discord_id, telegram_username, signal_number
379 ) VALUES ($1, $2, $3, $4, $5, $6, $7::notification_channel, $8, $9, $10) RETURNING id"#,
380 )
381 .bind(short_handle)
382 .bind(&email)
383 .bind(&did)
384 .bind(&password_hash)
385 .bind(&verification_code)
386 .bind(&code_expires_at)
387 .bind(verification_channel)
388 .bind(input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
389 .bind(input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
390 .bind(input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
391 .fetch_one(&mut *tx)
392 .await;
393 let user_id = match user_insert {
394 Ok((id,)) => id,
395 Err(e) => {
396 if let Some(db_err) = e.as_database_error() {
397 if db_err.code().as_deref() == Some("23505") {
398 let constraint = db_err.constraint().unwrap_or("");
399 if constraint.contains("handle") || constraint.contains("users_handle") {
400 return (
401 StatusCode::BAD_REQUEST,
402 Json(json!({
403 "error": "HandleNotAvailable",
404 "message": "Handle already taken"
405 })),
406 )
407 .into_response();
408 } else if constraint.contains("email") || constraint.contains("users_email") {
409 return (
410 StatusCode::BAD_REQUEST,
411 Json(json!({
412 "error": "InvalidEmail",
413 "message": "Email already registered"
414 })),
415 )
416 .into_response();
417 } else if constraint.contains("did") || constraint.contains("users_did") {
418 return (
419 StatusCode::BAD_REQUEST,
420 Json(json!({
421 "error": "AccountAlreadyExists",
422 "message": "An account with this DID already exists"
423 })),
424 )
425 .into_response();
426 }
427 }
428 }
429 error!("Error inserting user: {:?}", e);
430 return (
431 StatusCode::INTERNAL_SERVER_ERROR,
432 Json(json!({"error": "InternalError"})),
433 )
434 .into_response();
435 }
436 };
437 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
438 Ok(enc) => enc,
439 Err(e) => {
440 error!("Error encrypting user key: {:?}", e);
441 return (
442 StatusCode::INTERNAL_SERVER_ERROR,
443 Json(json!({"error": "InternalError"})),
444 )
445 .into_response();
446 }
447 };
448 let key_insert = sqlx::query!(
449 "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
450 user_id,
451 &encrypted_key_bytes[..],
452 crate::config::ENCRYPTION_VERSION
453 )
454 .execute(&mut *tx)
455 .await;
456 if let Err(e) = key_insert {
457 error!("Error inserting user key: {:?}", e);
458 return (
459 StatusCode::INTERNAL_SERVER_ERROR,
460 Json(json!({"error": "InternalError"})),
461 )
462 .into_response();
463 }
464 if let Some(key_id) = reserved_key_id {
465 let mark_used = sqlx::query!(
466 "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1",
467 key_id
468 )
469 .execute(&mut *tx)
470 .await;
471 if let Err(e) = mark_used {
472 error!("Error marking reserved key as used: {:?}", e);
473 return (
474 StatusCode::INTERNAL_SERVER_ERROR,
475 Json(json!({"error": "InternalError"})),
476 )
477 .into_response();
478 }
479 }
480 let mst = Mst::new(Arc::new(state.block_store.clone()));
481 let mst_root = match mst.persist().await {
482 Ok(c) => c,
483 Err(e) => {
484 error!("Error persisting MST: {:?}", e);
485 return (
486 StatusCode::INTERNAL_SERVER_ERROR,
487 Json(json!({"error": "InternalError"})),
488 )
489 .into_response();
490 }
491 };
492 let did_obj = match Did::new(&did) {
493 Ok(d) => d,
494 Err(_) => {
495 return (
496 StatusCode::INTERNAL_SERVER_ERROR,
497 Json(json!({"error": "InternalError", "message": "Invalid DID"})),
498 )
499 .into_response();
500 }
501 };
502 let rev = Tid::now(LimitedU32::MIN);
503 let unsigned_commit = Commit::new_unsigned(did_obj, mst_root, rev, None);
504 let signed_commit = match unsigned_commit.sign(&signing_key) {
505 Ok(c) => c,
506 Err(e) => {
507 error!("Error signing genesis commit: {:?}", e);
508 return (
509 StatusCode::INTERNAL_SERVER_ERROR,
510 Json(json!({"error": "InternalError"})),
511 )
512 .into_response();
513 }
514 };
515 let commit_bytes = match signed_commit.to_cbor() {
516 Ok(b) => b,
517 Err(e) => {
518 error!("Error serializing genesis commit: {:?}", e);
519 return (
520 StatusCode::INTERNAL_SERVER_ERROR,
521 Json(json!({"error": "InternalError"})),
522 )
523 .into_response();
524 }
525 };
526 let commit_cid = match state.block_store.put(&commit_bytes).await {
527 Ok(c) => c,
528 Err(e) => {
529 error!("Error saving genesis commit: {:?}", e);
530 return (
531 StatusCode::INTERNAL_SERVER_ERROR,
532 Json(json!({"error": "InternalError"})),
533 )
534 .into_response();
535 }
536 };
537 let commit_cid_str = commit_cid.to_string();
538 let repo_insert = sqlx::query!("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", user_id, commit_cid_str)
539 .execute(&mut *tx)
540 .await;
541 if let Err(e) = repo_insert {
542 error!("Error initializing repo: {:?}", e);
543 return (
544 StatusCode::INTERNAL_SERVER_ERROR,
545 Json(json!({"error": "InternalError"})),
546 )
547 .into_response();
548 }
549 if let Some(code) = &input.invite_code {
550 let use_insert =
551 sqlx::query!("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", code, user_id)
552 .execute(&mut *tx)
553 .await;
554 if let Err(e) = use_insert {
555 error!("Error recording invite usage: {:?}", e);
556 return (
557 StatusCode::INTERNAL_SERVER_ERROR,
558 Json(json!({"error": "InternalError"})),
559 )
560 .into_response();
561 }
562 }
563 if let Err(e) = tx.commit().await {
564 error!("Error committing transaction: {:?}", e);
565 return (
566 StatusCode::INTERNAL_SERVER_ERROR,
567 Json(json!({"error": "InternalError"})),
568 )
569 .into_response();
570 }
571 if let Err(e) = crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await {
572 warn!("Failed to sequence identity event for {}: {}", did, e);
573 }
574 if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await {
575 warn!("Failed to sequence account event for {}: {}", did, e);
576 }
577 let profile_record = json!({
578 "$type": "app.bsky.actor.profile",
579 "displayName": input.handle
580 });
581 if let Err(e) = crate::api::repo::record::create_record_internal(
582 &state,
583 &did,
584 "app.bsky.actor.profile",
585 "self",
586 &profile_record,
587 ).await {
588 warn!("Failed to create default profile for {}: {}", did, e);
589 }
590 if let Err(e) = crate::notifications::enqueue_signup_verification(
591 &state.db,
592 user_id,
593 verification_channel,
594 &verification_recipient,
595 &verification_code,
596 ).await {
597 warn!("Failed to enqueue signup verification notification: {:?}", e);
598 }
599 (
600 StatusCode::OK,
601 Json(CreateAccountOutput {
602 handle: short_handle.to_string(),
603 did,
604 verification_required: true,
605 verification_channel: verification_channel.to_string(),
606 }),
607 )
608 .into_response()
609}