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