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