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