forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
46 kB
1408 lines
1use axum::{
2 Form, Json,
3 extract::{Query, State},
4 http::HeaderMap,
5 response::{IntoResponse, Redirect, Response},
6};
7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
8use serde::{Deserialize, Serialize};
9use tranquil_db_traits::{SsoAction, SsoProviderType};
10use tranquil_types::RequestId;
11
12use tranquil_pds::api::error::ApiError;
13use tranquil_pds::auth::extractor::extract_auth_token_from_header;
14use tranquil_pds::auth::{generate_app_password, validate_bearer_token_cached};
15use tranquil_pds::rate_limit::{
16 AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit,
17 check_user_rate_limit_with_message,
18};
19use tranquil_pds::sso::SsoConfig;
20use tranquil_pds::state::AppState;
21
22fn generate_nonce() -> String {
23 use rand::RngCore;
24 let mut bytes = [0u8; 16];
25 rand::thread_rng().fill_bytes(&mut bytes);
26 URL_SAFE_NO_PAD.encode(bytes)
27}
28
29#[derive(Debug, Serialize)]
30pub struct SsoProviderInfo {
31 pub provider: SsoProviderType,
32 pub name: String,
33 pub icon: String,
34}
35
36#[derive(Debug, Serialize)]
37pub struct SsoProvidersResponse {
38 pub providers: Vec<SsoProviderInfo>,
39}
40
41pub async fn get_sso_providers(State(state): State<AppState>) -> Json<SsoProvidersResponse> {
42 let providers = state
43 .sso_manager
44 .enabled_providers()
45 .iter()
46 .map(|(t, name, icon)| SsoProviderInfo {
47 provider: *t,
48 name: name.to_string(),
49 icon: icon.to_string(),
50 })
51 .collect();
52
53 Json(SsoProvidersResponse { providers })
54}
55
56#[derive(Debug, Deserialize)]
57pub struct SsoInitiateRequest {
58 pub provider: SsoProviderType,
59 pub request_uri: Option<String>,
60 pub action: Option<SsoAction>,
61}
62
63#[derive(Debug, Serialize)]
64pub struct SsoInitiateResponse {
65 pub redirect_url: String,
66}
67
68pub async fn sso_initiate(
69 State(state): State<AppState>,
70 _rate_limit: RateLimited<SsoInitiateLimit>,
71 headers: HeaderMap,
72 Json(input): Json<SsoInitiateRequest>,
73) -> Result<Json<SsoInitiateResponse>, ApiError> {
74 if let Some(ref uri) = input.request_uri
75 && uri.len() > 500
76 {
77 return Err(ApiError::InvalidRequest("Request URI too long".into()));
78 }
79
80 let provider_type = input.provider;
81
82 let provider = state
83 .sso_manager
84 .get_provider(provider_type)
85 .ok_or(ApiError::SsoProviderNotEnabled)?;
86
87 let action = input.action.unwrap_or(SsoAction::Login);
88
89 let is_standalone = action == SsoAction::Register && input.request_uri.is_none();
90 let request_uri = input
91 .request_uri
92 .clone()
93 .unwrap_or_else(|| "standalone".to_string());
94
95 let auth_did = match action {
96 SsoAction::Link => {
97 let auth_header = headers
98 .get(axum::http::header::AUTHORIZATION)
99 .and_then(|v| v.to_str().ok());
100 let extracted =
101 extract_auth_token_from_header(auth_header).ok_or(ApiError::SsoNotAuthenticated)?;
102 let auth_user = validate_bearer_token_cached(
103 state.user_repo.as_ref(),
104 state.cache.as_ref(),
105 &extracted.token,
106 )
107 .await
108 .map_err(|_| ApiError::SsoNotAuthenticated)?;
109 Some(auth_user.did)
110 }
111 SsoAction::Register if is_standalone => None,
112 _ => {
113 let request_id = RequestId::new(request_uri.clone());
114 let _request_data = state
115 .oauth_repo
116 .get_authorization_request(&request_id)
117 .await?
118 .ok_or(ApiError::InvalidRequest(
119 "Authorization request not found or expired".into(),
120 ))?;
121 None
122 }
123 };
124
125 let sso_state = tranquil_pds::util::generate_random_token();
126 let nonce = generate_nonce();
127 let redirect_uri = SsoConfig::get_redirect_uri();
128
129 let auth_result = provider
130 .build_auth_url(&sso_state, redirect_uri, Some(&nonce))
131 .await
132 .map_err(|e| {
133 tracing::error!("Failed to build auth URL: {:?}", e);
134 ApiError::InternalError(Some("Failed to build authorization URL".into()))
135 })?;
136
137 state
138 .sso_repo
139 .create_sso_auth_state(
140 &sso_state,
141 &request_uri,
142 provider_type,
143 action,
144 Some(&nonce),
145 auth_result.code_verifier.as_deref(),
146 auth_did.as_ref(),
147 )
148 .await?;
149
150 tracing::debug!(
151 provider = %provider_type.as_str(),
152 action = %action,
153 "SSO flow initiated"
154 );
155
156 Ok(Json(SsoInitiateResponse {
157 redirect_url: auth_result.url,
158 }))
159}
160
161#[derive(Debug, Deserialize)]
162pub struct SsoCallbackQuery {
163 pub code: Option<String>,
164 pub state: Option<String>,
165 pub error: Option<String>,
166 pub error_description: Option<String>,
167}
168
169#[derive(Debug, Deserialize)]
170pub struct SsoCallbackForm {
171 pub code: Option<String>,
172 pub state: Option<String>,
173 pub error: Option<String>,
174 pub error_description: Option<String>,
175 #[serde(default)]
176 pub user: Option<String>,
177}
178
179fn redirect_to_error(message: &str) -> Response {
180 let encoded = urlencoding::encode(message);
181 Redirect::to(&format!("/app/oauth/error?error={}", encoded)).into_response()
182}
183
184fn redirect_to_login_with_error(request_uri: &str, message: &str) -> Response {
185 let uri_encoded = urlencoding::encode(request_uri);
186 let msg_encoded = urlencoding::encode(message);
187 Redirect::to(&format!(
188 "/app/oauth/login?request_uri={}&error={}",
189 uri_encoded, msg_encoded
190 ))
191 .into_response()
192}
193
194pub async fn sso_callback(
195 State(state): State<AppState>,
196 _rate_limit: RateLimited<SsoCallbackLimit>,
197 Query(query): Query<SsoCallbackQuery>,
198) -> Response {
199 sso_callback_internal(&state, query).await
200}
201
202async fn sso_callback_internal(state: &AppState, query: SsoCallbackQuery) -> Response {
203 tracing::debug!(
204 has_code = query.code.is_some(),
205 has_state = query.state.is_some(),
206 has_error = query.error.is_some(),
207 "SSO callback received"
208 );
209
210 if let Some(ref error) = query.error {
211 tracing::warn!(
212 error = %error,
213 error_description = ?query.error_description,
214 "SSO provider returned error"
215 );
216 if error.len() > 100 {
217 return redirect_to_error("Invalid error response");
218 }
219 let desc = query
220 .error_description
221 .as_ref()
222 .map(|d| if d.len() > 500 { "Error" } else { d.as_str() })
223 .unwrap_or_default();
224 return redirect_to_error(&format!("{}: {}", error, desc));
225 }
226
227 let (code, sso_state) = match (&query.code, &query.state) {
228 (Some(c), Some(s)) if c.len() <= 2000 && s.len() <= 100 => (c.clone(), s.clone()),
229 (Some(_), Some(_)) => return redirect_to_error("Invalid callback parameters"),
230 _ => return redirect_to_error("Missing code or state parameter"),
231 };
232
233 let auth_state = match state.sso_repo.consume_sso_auth_state(&sso_state).await {
234 Ok(Some(s)) => s,
235 Ok(None) => return redirect_to_error("SSO session expired or invalid"),
236 Err(e) => {
237 tracing::error!("SSO state lookup failed: {:?}", e);
238 return redirect_to_error("Database error");
239 }
240 };
241
242 tracing::debug!(
243 provider = %auth_state.provider.as_str(),
244 action = %auth_state.action,
245 request_uri = %auth_state.request_uri,
246 "SSO auth state retrieved"
247 );
248
249 let is_standalone = auth_state.request_uri == "standalone";
250
251 let provider = match state.sso_manager.get_provider(auth_state.provider) {
252 Some(p) => p,
253 None => return redirect_to_error("Provider no longer available"),
254 };
255
256 let redirect_uri = SsoConfig::get_redirect_uri();
257
258 let token_resp = match provider
259 .exchange_code(&code, redirect_uri, auth_state.code_verifier.as_deref())
260 .await
261 {
262 Ok(t) => t,
263 Err(e) => {
264 tracing::error!("SSO token exchange failed: {:?}", e);
265 if is_standalone {
266 return redirect_to_error(
267 "Failed to exchange authorization code. Please try again.",
268 );
269 }
270 return redirect_to_login_with_error(
271 &auth_state.request_uri,
272 "Failed to exchange authorization code",
273 );
274 }
275 };
276
277 let user_info = match provider
278 .get_user_info(
279 &token_resp.access_token,
280 token_resp.id_token.as_deref(),
281 auth_state.nonce.as_deref(),
282 )
283 .await
284 {
285 Ok(u) => u,
286 Err(e) => {
287 tracing::error!("SSO user info fetch failed: {:?}", e);
288 if is_standalone {
289 return redirect_to_error(
290 "Failed to get user information from provider. Please try again.",
291 );
292 }
293 return redirect_to_login_with_error(
294 &auth_state.request_uri,
295 "Failed to get user information from provider",
296 );
297 }
298 };
299
300 match auth_state.action {
301 SsoAction::Login => {
302 handle_sso_login(
303 state,
304 &auth_state.request_uri,
305 auth_state.provider,
306 &user_info,
307 )
308 .await
309 }
310 SsoAction::Link => {
311 let did = match auth_state.did {
312 Some(d) => d,
313 None => return redirect_to_error("Not authenticated"),
314 };
315 handle_sso_link(state, did, auth_state.provider, &user_info).await
316 }
317 SsoAction::Register => {
318 handle_sso_register(
319 state,
320 &auth_state.request_uri,
321 auth_state.provider,
322 &user_info,
323 )
324 .await
325 }
326 }
327}
328
329pub async fn sso_callback_post(
330 State(state): State<AppState>,
331 _rate_limit: RateLimited<SsoCallbackLimit>,
332 Form(form): Form<SsoCallbackForm>,
333) -> Response {
334 tracing::debug!(
335 has_code = form.code.is_some(),
336 has_state = form.state.is_some(),
337 has_error = form.error.is_some(),
338 has_user = form.user.is_some(),
339 "SSO callback (POST/form_post) received"
340 );
341
342 let query = SsoCallbackQuery {
343 code: form.code,
344 state: form.state,
345 error: form.error,
346 error_description: form.error_description,
347 };
348
349 sso_callback_internal(&state, query).await
350}
351
352fn generate_registration_token() -> String {
353 use rand::RngCore;
354 let mut bytes = [0u8; 32];
355 rand::thread_rng().fill_bytes(&mut bytes);
356 URL_SAFE_NO_PAD.encode(bytes)
357}
358
359async fn handle_sso_login(
360 state: &AppState,
361 request_uri: &str,
362 provider: SsoProviderType,
363 user_info: &tranquil_pds::sso::providers::SsoUserInfo,
364) -> Response {
365 let identity = match state
366 .sso_repo
367 .get_external_identity_by_provider(provider, &user_info.provider_user_id)
368 .await
369 {
370 Ok(Some(id)) => id,
371 Ok(None) => {
372 let token = generate_registration_token();
373 if let Err(e) = state
374 .sso_repo
375 .create_pending_registration(
376 &token,
377 request_uri,
378 provider,
379 &user_info.provider_user_id,
380 user_info.username.as_deref(),
381 user_info.email.as_deref(),
382 user_info.email_verified.unwrap_or(false),
383 )
384 .await
385 {
386 tracing::error!("Failed to create pending registration: {:?}", e);
387 return redirect_to_error("Database error");
388 }
389 return Redirect::to(&format!(
390 "/app/oauth/sso-register?token={}",
391 urlencoding::encode(&token),
392 ))
393 .into_response();
394 }
395 Err(e) => {
396 tracing::error!("SSO identity lookup failed: {:?}", e);
397 return redirect_to_error("Database error");
398 }
399 };
400
401 let is_verified = match state.user_repo.get_session_info_by_did(&identity.did).await {
402 Ok(Some(info)) => info.channel_verification.has_any_verified(),
403 Ok(None) => {
404 tracing::error!("User not found for SSO login: {}", identity.did);
405 return redirect_to_error("Account not found");
406 }
407 Err(e) => {
408 tracing::error!("Database error checking verification status: {:?}", e);
409 return redirect_to_error("Database error");
410 }
411 };
412
413 if !is_verified {
414 tracing::warn!(
415 did = %identity.did,
416 provider = %provider.as_str(),
417 "SSO login attempt for unverified account"
418 );
419 return redirect_to_login_with_error(
420 request_uri,
421 "Please verify your account before logging in",
422 );
423 }
424
425 if let Err(e) = state
426 .sso_repo
427 .update_external_identity_login(
428 identity.id,
429 user_info.username.as_deref(),
430 user_info.email.as_deref(),
431 )
432 .await
433 {
434 tracing::warn!("Failed to update external identity last login: {:?}", e);
435 }
436
437 let request_id = RequestId::new(request_uri.to_string());
438 if let Err(e) = state
439 .oauth_repo
440 .set_authorization_did(&request_id, &identity.did, None)
441 .await
442 {
443 tracing::error!("Failed to set authorization DID: {:?}", e);
444 return redirect_to_error("Failed to authenticate");
445 }
446
447 tracing::info!(
448 did = %identity.did,
449 provider = %provider.as_str(),
450 provider_user_id = %user_info.provider_user_id,
451 "SSO login successful"
452 );
453
454 let has_totp = matches!(
455 state.user_repo.get_totp_record_state(&identity.did).await,
456 Ok(Some(tranquil_db_traits::TotpRecordState::Verified(_)))
457 );
458
459 if has_totp {
460 return Redirect::to(&format!(
461 "/app/oauth/totp?request_uri={}",
462 urlencoding::encode(request_uri)
463 ))
464 .into_response();
465 }
466
467 Redirect::to(&format!(
468 "/app/oauth/consent?request_uri={}",
469 urlencoding::encode(request_uri)
470 ))
471 .into_response()
472}
473
474async fn handle_sso_link(
475 state: &AppState,
476 did: tranquil_types::Did,
477 provider: SsoProviderType,
478 user_info: &tranquil_pds::sso::providers::SsoUserInfo,
479) -> Response {
480 let existing = state
481 .sso_repo
482 .get_external_identity_by_provider(provider, &user_info.provider_user_id)
483 .await;
484
485 match existing {
486 Ok(Some(existing_id)) => {
487 if existing_id.did != did {
488 tracing::warn!(
489 provider = %provider.as_str(),
490 provider_user_id = %user_info.provider_user_id,
491 existing_did = %existing_id.did,
492 requested_did = %did,
493 "SSO account already linked to different user"
494 );
495 return Redirect::to(&format!(
496 "/app/security?error={}",
497 urlencoding::encode("This SSO account is already linked to a different user")
498 ))
499 .into_response();
500 }
501 tracing::info!(
502 did = %did,
503 provider = %provider.as_str(),
504 "SSO account already linked to this user"
505 );
506 return Redirect::to("/app/security?sso_linked=true").into_response();
507 }
508 Ok(None) => {}
509 Err(e) => {
510 tracing::error!("Failed to check existing identity: {:?}", e);
511 return Redirect::to(&format!(
512 "/app/security?error={}",
513 urlencoding::encode("Database error")
514 ))
515 .into_response();
516 }
517 }
518
519 if let Err(e) = state
520 .sso_repo
521 .create_external_identity(
522 &did,
523 provider,
524 &user_info.provider_user_id,
525 user_info.username.as_deref(),
526 user_info.email.as_deref(),
527 )
528 .await
529 {
530 tracing::error!("Failed to create external identity: {:?}", e);
531 return Redirect::to(&format!(
532 "/app/security?error={}",
533 urlencoding::encode("Failed to link account")
534 ))
535 .into_response();
536 }
537
538 tracing::info!(
539 did = %did,
540 provider = %provider.as_str(),
541 provider_user_id = %user_info.provider_user_id,
542 "Successfully linked SSO account"
543 );
544 Redirect::to("/app/security?sso_linked=true").into_response()
545}
546
547async fn handle_sso_register(
548 state: &AppState,
549 request_uri: &str,
550 provider: SsoProviderType,
551 user_info: &tranquil_pds::sso::providers::SsoUserInfo,
552) -> Response {
553 match state
554 .sso_repo
555 .get_external_identity_by_provider(provider, &user_info.provider_user_id)
556 .await
557 {
558 Ok(Some(_)) => {
559 return redirect_to_error(
560 "This account is already linked to an existing user. Please sign in instead.",
561 );
562 }
563 Ok(None) => {}
564 Err(e) => {
565 tracing::error!("SSO identity lookup failed: {:?}", e);
566 return redirect_to_error("Database error");
567 }
568 }
569
570 let token = generate_registration_token();
571 if let Err(e) = state
572 .sso_repo
573 .create_pending_registration(
574 &token,
575 request_uri,
576 provider,
577 &user_info.provider_user_id,
578 user_info.username.as_deref(),
579 user_info.email.as_deref(),
580 user_info.email_verified.unwrap_or(false),
581 )
582 .await
583 {
584 tracing::error!("Failed to create pending registration: {:?}", e);
585 return redirect_to_error("Database error");
586 }
587 Redirect::to(&format!(
588 "/app/oauth/sso-register?token={}",
589 urlencoding::encode(&token),
590 ))
591 .into_response()
592}
593
594#[derive(Debug, Serialize)]
595pub struct LinkedAccountInfo {
596 pub id: String,
597 pub provider: SsoProviderType,
598 pub provider_name: String,
599 pub provider_username: Option<String>,
600 pub provider_email: Option<String>,
601 pub created_at: String,
602 pub last_login_at: Option<String>,
603}
604
605#[derive(Debug, Serialize)]
606pub struct LinkedAccountsResponse {
607 pub accounts: Vec<LinkedAccountInfo>,
608}
609
610pub async fn get_linked_accounts(
611 State(state): State<AppState>,
612 auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
613) -> Result<Json<LinkedAccountsResponse>, ApiError> {
614 let identities = state
615 .sso_repo
616 .get_external_identities_by_did(&auth.did)
617 .await?;
618
619 let accounts = identities
620 .into_iter()
621 .map(|id| LinkedAccountInfo {
622 id: id.id.to_string(),
623 provider: id.provider,
624 provider_name: id.provider.display_name().to_string(),
625 provider_username: id.provider_username.map(|u| u.into_inner()),
626 provider_email: id.provider_email.map(|e| e.into_inner()),
627 created_at: id.created_at.to_rfc3339(),
628 last_login_at: id.last_login_at.map(|t| t.to_rfc3339()),
629 })
630 .collect();
631
632 Ok(Json(LinkedAccountsResponse { accounts }))
633}
634
635#[derive(Debug, Deserialize)]
636pub struct UnlinkAccountRequest {
637 pub id: String,
638}
639
640#[derive(Debug, Serialize)]
641pub struct UnlinkAccountResponse {
642 pub success: bool,
643}
644
645pub async fn unlink_account(
646 State(state): State<AppState>,
647 auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
648 Json(input): Json<UnlinkAccountRequest>,
649) -> Result<Json<UnlinkAccountResponse>, ApiError> {
650 let _rate_limit = check_user_rate_limit_with_message::<SsoUnlinkLimit>(
651 &state,
652 auth.did.as_str(),
653 "Too many unlink attempts. Please try again later.",
654 )
655 .await?;
656
657 let id = uuid::Uuid::parse_str(&input.id).map_err(|_| ApiError::InvalidId)?;
658
659 let has_password = state
660 .user_repo
661 .has_password_by_did(&auth.did)
662 .await?
663 .unwrap_or(false);
664
665 let passkeys = state.user_repo.get_passkeys_for_user(&auth.did).await?;
666 let has_passkeys = !passkeys.is_empty();
667
668 if !has_password && !has_passkeys {
669 let identities = state
670 .sso_repo
671 .get_external_identities_by_did(&auth.did)
672 .await?;
673
674 if identities.len() <= 1 {
675 return Err(ApiError::InvalidRequest(
676 "Cannot unlink your only login method. Add a password or passkey first."
677 .to_string(),
678 ));
679 }
680 }
681
682 let deleted = state
683 .sso_repo
684 .delete_external_identity(id, &auth.did)
685 .await?;
686
687 if !deleted {
688 return Err(ApiError::SsoLinkNotFound);
689 }
690
691 tracing::info!(did = %auth.did, identity_id = %id, "SSO account unlinked");
692
693 Ok(Json(UnlinkAccountResponse { success: true }))
694}
695
696#[derive(Debug, Deserialize)]
697pub struct PendingRegistrationQuery {
698 pub token: String,
699}
700
701#[derive(Debug, Serialize)]
702pub struct PendingRegistrationResponse {
703 pub request_uri: String,
704 pub provider: SsoProviderType,
705 pub provider_user_id: String,
706 pub provider_username: Option<String>,
707 pub provider_email: Option<String>,
708 pub provider_email_verified: bool,
709}
710
711pub async fn get_pending_registration(
712 State(state): State<AppState>,
713 _rate_limit: RateLimited<SsoCallbackLimit>,
714 Query(query): Query<PendingRegistrationQuery>,
715) -> Result<Json<PendingRegistrationResponse>, ApiError> {
716 if query.token.len() > 100 {
717 return Err(ApiError::InvalidRequest("Invalid token".into()));
718 }
719
720 let pending = state
721 .sso_repo
722 .get_pending_registration(&query.token)
723 .await?
724 .ok_or(ApiError::SsoSessionExpired)?;
725
726 Ok(Json(PendingRegistrationResponse {
727 request_uri: pending.request_uri,
728 provider: pending.provider,
729 provider_user_id: pending.provider_user_id.into_inner(),
730 provider_username: pending.provider_username.map(|u| u.into_inner()),
731 provider_email: pending.provider_email.map(|e| e.into_inner()),
732 provider_email_verified: pending.provider_email_verified,
733 }))
734}
735
736#[derive(Debug, Deserialize)]
737pub struct CheckHandleQuery {
738 pub handle: String,
739 pub domain: Option<String>,
740}
741
742#[derive(Debug, Serialize)]
743pub struct CheckHandleResponse {
744 pub available: bool,
745 pub reason: Option<String>,
746}
747
748pub async fn check_handle_available(
749 State(state): State<AppState>,
750 Query(query): Query<CheckHandleQuery>,
751) -> Result<Json<CheckHandleResponse>, ApiError> {
752 if query.handle.len() > 100 {
753 return Ok(Json(CheckHandleResponse {
754 available: false,
755 reason: Some("Handle too long".into()),
756 }));
757 }
758
759 let validated = match tranquil_pds::api::validation::validate_short_handle(&query.handle) {
760 Ok(h) => h,
761 Err(e) => {
762 return Ok(Json(CheckHandleResponse {
763 available: false,
764 reason: Some(e.to_string()),
765 }));
766 }
767 };
768
769 let available_domains = tranquil_config::get().server.available_user_domain_list();
770 if let Some(ref d) = query.domain
771 && !available_domains.iter().any(|ad| ad == d)
772 {
773 return Err(ApiError::InvalidRequest("Unknown user domain".into()));
774 }
775 let domain = query.domain.as_deref().unwrap_or(&available_domains[0]);
776 let full_handle = format!("{}.{}", validated, domain);
777 let handle_typed: tranquil_pds::types::Handle = match full_handle.parse() {
778 Ok(h) => h,
779 Err(_) => return Err(ApiError::InvalidHandle(None)),
780 };
781
782 let db_available = state
783 .user_repo
784 .check_handle_available_for_new_account(&handle_typed)
785 .await
786 .unwrap_or(false);
787
788 if !db_available {
789 return Ok(Json(CheckHandleResponse {
790 available: false,
791 reason: Some("Handle is already taken".into()),
792 }));
793 }
794
795 Ok(Json(CheckHandleResponse {
796 available: true,
797 reason: None,
798 }))
799}
800
801#[derive(Debug, Deserialize)]
802pub struct CompleteRegistrationInput {
803 pub token: String,
804 pub handle: String,
805 pub email: Option<String>,
806 pub invite_code: Option<String>,
807 pub verification_channel: Option<tranquil_db_traits::CommsChannel>,
808 pub discord_username: Option<String>,
809 pub telegram_username: Option<String>,
810 pub signal_username: Option<String>,
811 pub did_type: Option<String>,
812 pub did: Option<String>,
813}
814
815#[derive(Debug, Serialize)]
816#[serde(rename_all = "camelCase")]
817pub struct CompleteRegistrationResponse {
818 pub did: String,
819 pub handle: String,
820 pub redirect_url: String,
821 #[serde(skip_serializing_if = "Option::is_none")]
822 pub access_jwt: Option<String>,
823 #[serde(skip_serializing_if = "Option::is_none")]
824 pub refresh_jwt: Option<String>,
825 #[serde(skip_serializing_if = "Option::is_none")]
826 pub app_password: Option<String>,
827 #[serde(skip_serializing_if = "Option::is_none")]
828 pub app_password_name: Option<String>,
829}
830
831pub async fn complete_registration(
832 State(state): State<AppState>,
833 rate_limit: RateLimited<AccountCreationLimit>,
834 Json(input): Json<CompleteRegistrationInput>,
835) -> Result<Json<CompleteRegistrationResponse>, ApiError> {
836 let client_ip = rate_limit.client_ip();
837 use jacquard_common::types::{integer::LimitedU32, string::Tid};
838 use jacquard_repo::{mst::Mst, storage::BlockStore};
839 use k256::ecdsa::SigningKey;
840 use rand::rngs::OsRng;
841 use serde_json::json;
842 use std::sync::Arc;
843
844 if input.token.len() > 100 {
845 return Err(ApiError::InvalidRequest("Invalid token".into()));
846 }
847
848 if input.handle.len() > 100 {
849 return Err(ApiError::InvalidHandle(None));
850 }
851
852 let pending_preview = state
853 .sso_repo
854 .get_pending_registration(&input.token)
855 .await?
856 .ok_or(ApiError::SsoSessionExpired)?;
857
858 let cfg = tranquil_config::get();
859 let hostname = &cfg.server.hostname;
860 let available_domains = cfg.server.available_user_domain_list();
861
862 let matched_domain = available_domains
863 .iter()
864 .filter(|d| input.handle.ends_with(&format!(".{}", d)))
865 .max_by_key(|d| d.len());
866
867 let handle = if !input.handle.contains('.') || matched_domain.is_some() {
868 let handle_to_validate = match matched_domain {
869 Some(domain) => input
870 .handle
871 .strip_suffix(&format!(".{}", domain))
872 .unwrap_or(&input.handle),
873 None => &input.handle,
874 };
875 match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) {
876 Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
877 Err(_) => return Err(ApiError::InvalidHandle(None)),
878 }
879 } else {
880 match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) {
881 Ok(h) => h,
882 Err(_) => return Err(ApiError::InvalidHandle(None)),
883 }
884 };
885
886 let verification_channel = input
887 .verification_channel
888 .unwrap_or(tranquil_db_traits::CommsChannel::Email);
889 let verification_recipient = match verification_channel {
890 tranquil_db_traits::CommsChannel::Email => {
891 let email = input
892 .email
893 .clone()
894 .or_else(|| {
895 pending_preview
896 .provider_email
897 .clone()
898 .map(|e| e.into_inner())
899 })
900 .map(|e| e.trim().to_string())
901 .filter(|e| !e.is_empty());
902 match email {
903 Some(e) if !e.is_empty() => e,
904 _ => return Err(ApiError::MissingEmail),
905 }
906 }
907 tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
908 Some(username) if !username.trim().is_empty() => {
909 let clean = username.trim().to_lowercase();
910 if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
911 return Err(ApiError::InvalidRequest(
912 "Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
913 ));
914 }
915 clean
916 }
917 _ => return Err(ApiError::MissingDiscordId),
918 },
919 tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
920 Some(username) if !username.trim().is_empty() => {
921 let clean = username.trim().trim_start_matches('@');
922 if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
923 return Err(ApiError::InvalidRequest(
924 "Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
925 ));
926 }
927 clean.to_string()
928 }
929 _ => return Err(ApiError::MissingTelegramUsername),
930 },
931 tranquil_db_traits::CommsChannel::Signal => match &input.signal_username {
932 Some(username) if !username.trim().is_empty() => {
933 username.trim().trim_start_matches('@').to_lowercase()
934 }
935 _ => return Err(ApiError::MissingSignalNumber),
936 },
937 };
938
939 let email = input
940 .email
941 .clone()
942 .or_else(|| {
943 pending_preview
944 .provider_email
945 .clone()
946 .map(|e| e.into_inner())
947 })
948 .map(|e| e.trim().to_string())
949 .filter(|e| !e.is_empty());
950
951 let email = match &email {
952 Some(e) => {
953 if e.len() > 254 {
954 return Err(ApiError::InvalidEmail);
955 }
956 if !tranquil_pds::api::validation::is_valid_email(e) {
957 return Err(ApiError::InvalidEmail);
958 }
959 Some(e.clone())
960 }
961 None => None,
962 };
963
964 let _validated_invite_code = if let Some(ref code) = input.invite_code {
965 match state.infra_repo.validate_invite_code(code).await {
966 Ok(validated) => Some(validated),
967 Err(_) => return Err(ApiError::InvalidInviteCode),
968 }
969 } else {
970 let invite_required = tranquil_config::get().server.invite_code_required;
971 if invite_required {
972 return Err(ApiError::InviteCodeRequired);
973 }
974 None
975 };
976
977 let handle_typed: tranquil_pds::types::Handle =
978 handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
979 let reserved = state
980 .user_repo
981 .reserve_handle(&handle_typed, client_ip)
982 .await
983 .unwrap_or(false);
984
985 if !reserved {
986 return Err(ApiError::HandleNotAvailable(None));
987 }
988
989 let secret_key = k256::SecretKey::random(&mut OsRng);
990 let secret_key_bytes = secret_key.to_bytes().to_vec();
991 let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
992 Ok(k) => k,
993 Err(e) => {
994 tracing::error!("Error creating signing key: {:?}", e);
995 return Err(ApiError::InternalError(None));
996 }
997 };
998
999 let pds_endpoint = format!("https://{}", hostname);
1000 let did_type = input.did_type.as_deref().unwrap_or("plc");
1001
1002 let did = match did_type {
1003 "web" => {
1004 if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
1005 return Err(ApiError::SelfHostedDidWebDisabled);
1006 }
1007 let encoded_handle = handle.replace(':', "%3A");
1008 let self_hosted_did = format!("did:web:{}", encoded_handle);
1009 tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account");
1010 self_hosted_did
1011 }
1012 "web-external" => {
1013 let d = match &input.did {
1014 Some(d) if !d.trim().is_empty() => d.trim(),
1015 _ => {
1016 return Err(ApiError::InvalidRequest(
1017 "External did:web requires the 'did' field to be provided".into(),
1018 ));
1019 }
1020 };
1021 if !d.starts_with("did:web:") {
1022 return Err(ApiError::InvalidDid(
1023 "External DID must be a did:web".into(),
1024 ));
1025 }
1026 tracing::info!(did = %d, "Creating external did:web SSO account");
1027 d.to_string()
1028 }
1029 _ => {
1030 let rotation_key = tranquil_config::get()
1031 .secrets
1032 .plc_rotation_key
1033 .clone()
1034 .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key));
1035
1036 let genesis_result = match tranquil_pds::plc::create_genesis_operation(
1037 &signing_key,
1038 &rotation_key,
1039 &handle,
1040 &pds_endpoint,
1041 ) {
1042 Ok(r) => r,
1043 Err(e) => {
1044 tracing::error!("Error creating PLC genesis operation: {:?}", e);
1045 return Err(ApiError::InternalError(Some(
1046 "Failed to create PLC operation".into(),
1047 )));
1048 }
1049 };
1050
1051 let plc_client =
1052 tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
1053 if let Err(e) = plc_client
1054 .send_operation(&genesis_result.did, &genesis_result.signed_operation)
1055 .await
1056 {
1057 tracing::error!("Failed to submit PLC genesis operation: {:?}", e);
1058 return Err(ApiError::UpstreamErrorMsg(format!(
1059 "Failed to register DID with PLC directory: {}",
1060 e
1061 )));
1062 }
1063 genesis_result.did
1064 }
1065 };
1066 tracing::info!(did = %did, handle = %handle, provider = %pending_preview.provider.as_str(), "Created DID for SSO account");
1067
1068 let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) {
1069 Ok(bytes) => bytes,
1070 Err(e) => {
1071 tracing::error!("Error encrypting signing key: {:?}", e);
1072 return Err(ApiError::InternalError(None));
1073 }
1074 };
1075
1076 let mst = Mst::new(Arc::new(state.block_store.clone()));
1077 let mst_root = match mst.persist().await {
1078 Ok(c) => c,
1079 Err(e) => {
1080 tracing::error!("Error persisting MST: {:?}", e);
1081 return Err(ApiError::InternalError(None));
1082 }
1083 };
1084
1085 let rev = Tid::now(LimitedU32::MIN);
1086 let did_typed: tranquil_pds::types::Did = did
1087 .parse()
1088 .map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?;
1089 let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit(
1090 &did_typed,
1091 mst_root,
1092 rev.as_ref(),
1093 None,
1094 &signing_key,
1095 ) {
1096 Ok(result) => result,
1097 Err(e) => {
1098 tracing::error!("Error creating genesis commit: {:?}", e);
1099 return Err(ApiError::InternalError(None));
1100 }
1101 };
1102
1103 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
1104 Ok(c) => c,
1105 Err(e) => {
1106 tracing::error!("Error saving genesis commit: {:?}", e);
1107 return Err(ApiError::InternalError(None));
1108 }
1109 };
1110
1111 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
1112
1113 let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
1114 Some(json!({
1115 "$type": "app.bsky.actor.defs#personalDetailsPref",
1116 "birthDate": "1998-05-06T00:00:00.000Z"
1117 }))
1118 } else {
1119 None
1120 };
1121
1122 let create_input = tranquil_db_traits::CreateSsoAccountInput {
1123 handle: handle_typed.clone(),
1124 email: email.clone(),
1125 did: did_typed.clone(),
1126 preferred_comms_channel: verification_channel,
1127 discord_username: input
1128 .discord_username
1129 .clone()
1130 .map(|s| s.trim().to_lowercase())
1131 .filter(|s| !s.is_empty()),
1132 telegram_username: input
1133 .telegram_username
1134 .clone()
1135 .map(|s| s.trim().trim_start_matches('@').to_string())
1136 .filter(|s| !s.is_empty()),
1137 signal_username: input
1138 .signal_username
1139 .clone()
1140 .map(|s| s.trim().trim_start_matches('@').to_lowercase())
1141 .filter(|s| !s.is_empty()),
1142 encrypted_key_bytes: encrypted_key_bytes.clone(),
1143 encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
1144 commit_cid: commit_cid.to_string(),
1145 repo_rev: rev.as_ref().to_string(),
1146 genesis_block_cids,
1147 invite_code: input.invite_code.clone(),
1148 birthdate_pref,
1149 sso_provider: pending_preview.provider,
1150 sso_provider_user_id: pending_preview.provider_user_id.clone().into_inner(),
1151 sso_provider_username: pending_preview
1152 .provider_username
1153 .clone()
1154 .map(|u| u.into_inner()),
1155 sso_provider_email: pending_preview
1156 .provider_email
1157 .clone()
1158 .map(|e| e.into_inner()),
1159 sso_provider_email_verified: pending_preview.provider_email_verified,
1160 pending_registration_token: input.token.clone(),
1161 };
1162
1163 let create_result = match state.user_repo.create_sso_account(&create_input).await {
1164 Ok(r) => r,
1165 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
1166 return Err(ApiError::HandleNotAvailable(None));
1167 }
1168 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => {
1169 return Err(ApiError::EmailTaken);
1170 }
1171 Err(tranquil_db_traits::CreateAccountError::InvalidToken) => {
1172 return Err(ApiError::SsoSessionExpired);
1173 }
1174 Err(e) => {
1175 tracing::error!("Error creating SSO account: {:?}", e);
1176 return Err(ApiError::InternalError(None));
1177 }
1178 };
1179
1180 let _ = state
1181 .user_repo
1182 .release_handle_reservation(&handle_typed)
1183 .await;
1184
1185 if let Err(e) =
1186 tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed))
1187 .await
1188 {
1189 tracing::warn!("Failed to sequence identity event for {}: {}", did, e);
1190 }
1191 if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
1192 &state,
1193 &did_typed,
1194 tranquil_db_traits::AccountStatus::Active,
1195 )
1196 .await
1197 {
1198 tracing::warn!("Failed to sequence account event for {}: {}", did, e);
1199 }
1200
1201 let profile_record = json!({
1202 "$type": "app.bsky.actor.profile",
1203 "displayName": handle_typed.as_str()
1204 });
1205 if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
1206 &state,
1207 &did_typed,
1208 &tranquil_pds::types::PROFILE_COLLECTION,
1209 &tranquil_pds::types::PROFILE_RKEY,
1210 &profile_record,
1211 )
1212 .await
1213 {
1214 tracing::warn!("Failed to create default profile for {}: {}", did, e);
1215 }
1216
1217 let app_password = generate_app_password();
1218 let app_password_name = "bsky.app".to_string();
1219 let app_password_hash = match bcrypt::hash(&app_password, bcrypt::DEFAULT_COST) {
1220 Ok(h) => h,
1221 Err(e) => {
1222 tracing::error!("Failed to hash app password: {:?}", e);
1223 return Err(ApiError::InternalError(None));
1224 }
1225 };
1226
1227 let app_password_data = tranquil_db_traits::AppPasswordCreate {
1228 user_id: create_result.user_id,
1229 name: app_password_name.clone(),
1230 password_hash: app_password_hash,
1231 privilege: tranquil_db_traits::AppPasswordPrivilege::Standard,
1232 scopes: None,
1233 created_by_controller_did: None,
1234 };
1235 if let Err(e) = state
1236 .session_repo
1237 .create_app_password(&app_password_data)
1238 .await
1239 {
1240 tracing::warn!("Failed to create initial app password: {:?}", e);
1241 }
1242
1243 let is_standalone = pending_preview.request_uri == "standalone";
1244
1245 if !is_standalone {
1246 let request_id = RequestId::new(pending_preview.request_uri.clone());
1247 if let Err(e) = state
1248 .oauth_repo
1249 .set_authorization_did(&request_id, &did_typed, None)
1250 .await
1251 {
1252 tracing::error!("Failed to set authorization DID: {:?}", e);
1253 return Err(ApiError::InternalError(None));
1254 }
1255 }
1256
1257 tracing::info!(
1258 did = %did,
1259 handle = %handle,
1260 provider = %pending_preview.provider.as_str(),
1261 provider_user_id = %pending_preview.provider_user_id,
1262 standalone = %is_standalone,
1263 "SSO registration completed successfully"
1264 );
1265
1266 let user_id = state
1267 .user_repo
1268 .get_id_by_did(&did_typed)
1269 .await
1270 .unwrap_or(None);
1271
1272 let channel_auto_verified = verification_channel == tranquil_db_traits::CommsChannel::Email
1273 && pending_preview.provider_email_verified
1274 && pending_preview.provider_email.as_ref().map(|e| e.as_str()) == email.as_deref();
1275
1276 if channel_auto_verified {
1277 let _ = state
1278 .user_repo
1279 .set_channel_verified(&did_typed, tranquil_db_traits::CommsChannel::Email)
1280 .await;
1281 tracing::info!(did = %did, "Auto-verified email from SSO provider");
1282
1283 if is_standalone {
1284 let key_bytes = match tranquil_pds::config::decrypt_key(
1285 &encrypted_key_bytes,
1286 Some(tranquil_pds::config::ENCRYPTION_VERSION),
1287 ) {
1288 Ok(k) => k,
1289 Err(e) => {
1290 tracing::error!("Failed to decrypt user key: {:?}", e);
1291 return Err(ApiError::InternalError(None));
1292 }
1293 };
1294
1295 let access_meta =
1296 match tranquil_pds::auth::create_access_token_with_metadata(&did, &key_bytes) {
1297 Ok(m) => m,
1298 Err(e) => {
1299 tracing::error!("Failed to create access token: {:?}", e);
1300 return Err(ApiError::InternalError(None));
1301 }
1302 };
1303 let refresh_meta =
1304 match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &key_bytes) {
1305 Ok(m) => m,
1306 Err(e) => {
1307 tracing::error!("Failed to create refresh token: {:?}", e);
1308 return Err(ApiError::InternalError(None));
1309 }
1310 };
1311
1312 let session_data = tranquil_db_traits::SessionTokenCreate {
1313 did: did_typed.clone(),
1314 access_jti: access_meta.jti.clone(),
1315 refresh_jti: refresh_meta.jti.clone(),
1316 access_expires_at: access_meta.expires_at,
1317 refresh_expires_at: refresh_meta.expires_at,
1318 login_type: tranquil_db_traits::LoginType::Modern,
1319 mfa_verified: false,
1320 scope: Some("transition:generic".to_string()),
1321 controller_did: None,
1322 app_password_name: None,
1323 };
1324 if let Err(e) = state.session_repo.create_session(&session_data).await {
1325 tracing::error!("Failed to insert session: {:?}", e);
1326 return Err(ApiError::InternalError(None));
1327 }
1328
1329 let hostname = &tranquil_config::get().server.hostname;
1330 if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome(
1331 state.user_repo.as_ref(),
1332 state.infra_repo.as_ref(),
1333 user_id.unwrap_or(uuid::Uuid::nil()),
1334 hostname,
1335 )
1336 .await
1337 {
1338 tracing::warn!("Failed to enqueue welcome notification: {:?}", e);
1339 }
1340
1341 return Ok(Json(CompleteRegistrationResponse {
1342 did,
1343 handle,
1344 redirect_url: "/app/dashboard".to_string(),
1345 access_jwt: Some(access_meta.token),
1346 refresh_jwt: Some(refresh_meta.token),
1347 app_password: Some(app_password),
1348 app_password_name: Some(app_password_name),
1349 }));
1350 }
1351
1352 return Ok(Json(CompleteRegistrationResponse {
1353 did,
1354 handle,
1355 redirect_url: format!(
1356 "/app/oauth/consent?request_uri={}",
1357 urlencoding::encode(&pending_preview.request_uri)
1358 ),
1359 access_jwt: None,
1360 refresh_jwt: None,
1361 app_password: Some(app_password),
1362 app_password_name: Some(app_password_name),
1363 }));
1364 }
1365
1366 if let Some(uid) = user_id {
1367 let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
1368 &did_typed,
1369 verification_channel,
1370 &verification_recipient,
1371 );
1372 let formatted_token =
1373 tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
1374 if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
1375 state.user_repo.as_ref(),
1376 state.infra_repo.as_ref(),
1377 uid,
1378 verification_channel,
1379 &verification_recipient,
1380 &formatted_token,
1381 hostname,
1382 )
1383 .await
1384 {
1385 tracing::warn!("Failed to enqueue signup verification: {:?}", e);
1386 }
1387 }
1388
1389 let redirect_url = if is_standalone {
1390 format!("/app/verify?did={}", urlencoding::encode(&did))
1391 } else {
1392 format!(
1393 "/app/verify?did={}&request_uri={}",
1394 urlencoding::encode(&did),
1395 urlencoding::encode(&pending_preview.request_uri)
1396 )
1397 };
1398
1399 Ok(Json(CompleteRegistrationResponse {
1400 did,
1401 handle,
1402 redirect_url,
1403 access_jwt: None,
1404 refresh_jwt: None,
1405 app_password: Some(app_password),
1406 app_password_name: Some(app_password_name),
1407 }))
1408}