1#![recursion_limit = "256"]
2
3pub mod callback;
39pub mod config;
40pub mod errors;
41pub mod handlers;
42pub mod middleware;
43pub mod models;
44pub mod repositories;
45pub mod services;
46pub mod storage;
47pub mod utils;
48
49mod router;
50
51#[cfg(test)]
52pub(crate) mod test_env;
53
54pub use callback::{AuthCallback, AuthCallbackPayload, NoopCallback, ReferralRewardPayload};
55pub use config::{Config, DatabaseConfig, NotificationConfig};
56pub use errors::AppError;
57pub use router::create_router;
58pub use services::NotificationService;
60pub use services::ReferralPayoutWorker;
61pub use services::{
62 EmailService, InstantLinkEmailData, LogEmailService, NoopEmailService, PasswordResetEmailData,
63 VerificationEmailData,
64};
65#[cfg(feature = "postgres")]
66pub use sqlx::PgPool;
67pub use storage::Storage;
68
69use axum::Router;
70use repositories::{
71 ApiKeyRepository, AuditLogRepository, CredentialRepository, CreditHoldRepository,
72 CreditRefundRequestRepository, CreditRepository, CustomRoleRepository, DepositRepository,
73 DerivedWalletRepository, InviteRepository, LoginAttemptConfig, LoginAttemptRepository,
74 MembershipRepository, NonceRepository, OrgRepository, OutboxRepository, PolicyRepository,
75 PrivacyNoteRepository, ReferralCodeHistoryRepository, ReferralPayoutRepository,
76 SessionRepository, SystemSettingsRepository, TotpRepository, TreasuryConfigRepository,
77 UserRepository, UserWithdrawalLogRepository, VerificationRepository, WalletMaterialRepository,
78 WalletRotationHistoryRepository, WebAuthnRepository,
79};
80use services::{
81 create_wallet_unlock_cache, AppleService, AuditService, CommsService, DepositCreditService,
82 DepositFeeService, EncryptionService, GoogleService, JupiterSwapService, JwtService,
83 MfaAttemptService, NoteEncryptionService, OidcService, PasswordService, PrivacySidecarClient,
84 SanctionsService, SettingsService, SidecarClientConfig, SignupGatingService, SolPriceService,
85 SolanaService, StepUpService, TokenGatingService, TotpService, WalletSigningService,
86 WalletUnlockCache, WebAuthnService,
87};
88use std::sync::Arc;
89use utils::TokenCipher;
90
91fn build_privacy_sidecar_client(config: &Config) -> Result<PrivacySidecarClient, AppError> {
92 let api_key = config
93 .privacy
94 .sidecar_api_key
95 .clone()
96 .ok_or_else(|| AppError::Config("SIDECAR_API_KEY is required".into()))?;
97 PrivacySidecarClient::new(SidecarClientConfig {
98 base_url: config.privacy.sidecar_url.clone(),
99 timeout_ms: config.privacy.sidecar_timeout_ms,
100 api_key,
101 })
102}
103
104fn decode_note_encryption_key(key: &str) -> Result<Vec<u8>, base64::DecodeError> {
105 use base64::{engine::general_purpose::STANDARD, Engine as _};
106
107 STANDARD.decode(key)
108}
109
110fn build_note_encryption_service(
111 key_bytes: &[u8],
112 key_id: &str,
113) -> Result<NoteEncryptionService, AppError> {
114 NoteEncryptionService::new(key_bytes, key_id)
115}
116
117fn preload_settings_cache(settings_service: &Arc<SettingsService>) {
118 if let Ok(handle) = tokio::runtime::Handle::try_current() {
119 if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread {
120 tokio::task::block_in_place(|| {
121 if let Err(error) = handle.block_on(settings_service.refresh()) {
122 tracing::warn!(error = %error, "Failed to preload settings cache during router setup");
123 }
124 });
125 }
126 }
127}
128
129fn auto_generate_sidecar_secrets(
135 repo: &Arc<dyn SystemSettingsRepository>,
136 encryption: &EncryptionService,
137) {
138 use base64::{engine::general_purpose::STANDARD, Engine as _};
139 use rand::{rngs::OsRng, RngCore};
140
141 let Ok(handle) = tokio::runtime::Handle::try_current() else {
142 return;
143 };
144 if handle.runtime_flavor() != tokio::runtime::RuntimeFlavor::MultiThread {
145 return;
146 }
147
148 tokio::task::block_in_place(|| {
149 handle.block_on(async {
150 let keys_to_generate: Vec<(&str, Box<dyn Fn() -> String>)> = vec![
151 (
152 "sidecar_api_key",
153 Box::new(|| {
154 let mut bytes = [0u8; 32];
155 OsRng.fill_bytes(&mut bytes);
156 hex::encode(bytes)
157 }),
158 ),
159 (
160 "note_encryption_key",
161 Box::new(|| {
162 let mut bytes = [0u8; 32];
163 OsRng.fill_bytes(&mut bytes);
164 STANDARD.encode(bytes)
165 }),
166 ),
167 ];
168
169 for (key, generate) in keys_to_generate {
170 match repo.get_by_key(key).await {
172 Ok(Some(setting)) if !setting.value.is_empty() => {
173 tracing::debug!(key, "Sidecar secret already set, skipping auto-generation");
174 }
175 Ok(_) => {
176 let raw_value = generate();
178 let encrypted_value = match encryption.encrypt(&raw_value) {
179 Ok(v) => v,
180 Err(e) => {
181 tracing::error!(key, error = %e, "Failed to encrypt auto-generated sidecar secret");
182 continue;
183 }
184 };
185 let setting = repositories::SystemSetting {
186 key: key.to_string(),
187 value: encrypted_value,
188 category: "privacy".to_string(),
189 description: None,
190 is_secret: true,
191 encryption_version: Some("v1".to_string()),
192 updated_at: chrono::Utc::now(),
193 updated_by: None,
194 };
195 match repo.upsert_many(vec![setting]).await {
196 Ok(_) => tracing::info!(key, "Auto-generated sidecar secret"),
197 Err(e) => tracing::error!(key, error = %e, "Failed to persist auto-generated sidecar secret"),
198 }
199 }
200 Err(e) => {
201 tracing::warn!(key, error = %e, "Failed to check sidecar secret, skipping auto-generation");
202 }
203 }
204 }
205 });
206 });
207}
208
209fn read_sidecar_secret_sync(settings_service: &Arc<SettingsService>, key: &str) -> Option<String> {
212 let Ok(handle) = tokio::runtime::Handle::try_current() else {
213 return None;
214 };
215 if handle.runtime_flavor() != tokio::runtime::RuntimeFlavor::MultiThread {
216 return None;
217 }
218 tokio::task::block_in_place(|| {
219 handle.block_on(async {
220 match settings_service.get_secret(key).await {
221 Ok(Some(v)) if !v.is_empty() => Some(v),
222 _ => None,
223 }
224 })
225 })
226}
227
228pub struct AppState<C: AuthCallback, E: EmailService = LogEmailService> {
230 pub config: Config,
231 pub callback: Arc<C>,
232 pub jwt_service: JwtService,
233 pub password_service: PasswordService,
234 pub google_service: GoogleService,
235 pub apple_service: AppleService,
236 pub solana_service: SolanaService,
237 pub totp_service: TotpService,
238 pub webauthn_service: WebAuthnService,
239 pub oidc_service: OidcService,
240 pub encryption_service: EncryptionService,
241 pub phantom_email: std::marker::PhantomData<E>,
242 pub audit_service: AuditService,
243 pub comms_service: CommsService,
244 pub user_repo: Arc<dyn UserRepository>,
245 pub session_repo: Arc<dyn SessionRepository>,
246 pub nonce_repo: Arc<dyn NonceRepository>,
247 pub verification_repo: Arc<dyn VerificationRepository>,
248 pub org_repo: Arc<dyn OrgRepository>,
249 pub membership_repo: Arc<dyn MembershipRepository>,
250 pub invite_repo: Arc<dyn InviteRepository>,
251 pub audit_repo: Arc<dyn AuditLogRepository>,
252 pub login_attempt_repo: Arc<dyn LoginAttemptRepository>,
253 pub login_attempt_config: LoginAttemptConfig,
254 pub totp_repo: Arc<dyn TotpRepository>,
255 pub custom_role_repo: Arc<dyn CustomRoleRepository>,
256 pub policy_repo: Arc<dyn PolicyRepository>,
257 pub outbox_repo: Arc<dyn OutboxRepository>,
258 pub api_key_repo: Arc<dyn ApiKeyRepository>,
259 pub wallet_material_repo: Arc<dyn WalletMaterialRepository>,
260 pub derived_wallet_repo: Arc<dyn DerivedWalletRepository>,
261 pub wallet_rotation_history_repo: Arc<dyn WalletRotationHistoryRepository>,
262 pub credential_repo: Arc<dyn CredentialRepository>,
263 pub webauthn_repo: Arc<dyn WebAuthnRepository>,
264 pub deposit_repo: Arc<dyn DepositRepository>,
265 pub credit_repo: Arc<dyn CreditRepository>,
266 pub credit_hold_repo: Arc<dyn CreditHoldRepository>,
267 pub credit_refund_request_repo: Arc<dyn CreditRefundRequestRepository>,
268 pub privacy_note_repo: Arc<dyn PrivacyNoteRepository>,
269 pub system_settings_repo: Arc<dyn SystemSettingsRepository>,
271 pub treasury_config_repo: Arc<dyn TreasuryConfigRepository>,
273 pub user_withdrawal_log_repo: Arc<dyn UserWithdrawalLogRepository>,
275 pub referral_payout_repo: Arc<dyn ReferralPayoutRepository>,
277 pub referral_code_history_repo: Arc<dyn ReferralCodeHistoryRepository>,
279 pub settings_service: Arc<SettingsService>,
281 pub mfa_attempt_service: MfaAttemptService,
283 pub step_up_service: StepUpService,
284 pub wallet_signing_service: WalletSigningService,
286 pub wallet_unlock_cache: Arc<WalletUnlockCache>,
288 pub storage: Storage,
290 pub privacy_sidecar_client: Option<Arc<PrivacySidecarClient>>,
292 pub note_encryption_service: Option<Arc<NoteEncryptionService>>,
294 pub sol_price_service: Arc<SolPriceService>,
296 pub jupiter_swap_service: Option<Arc<JupiterSwapService>>,
298 pub deposit_credit_service: Arc<DepositCreditService>,
300 pub kyc_service: Option<Arc<services::KycService>>,
302 pub accreditation_service: Option<Arc<services::AccreditationService>>,
304 pub sanctions_service: Arc<SanctionsService>,
306 pub token_gating_service: Arc<TokenGatingService>,
308 pub signup_gating_service: Arc<SignupGatingService>,
310 #[cfg(feature = "postgres")]
311 pub postgres_pool: Option<PgPool>,
312}
313
314pub fn router<C: AuthCallback + 'static>(config: Config, callback: Arc<C>) -> Router {
319 router_with_storage(config, callback, Storage::in_memory())
320}
321
322pub fn router_with_storage<C: AuthCallback + 'static>(
338 config: Config,
339 callback: Arc<C>,
340 storage: Storage,
341) -> Router {
342 let settings_service = Arc::new(SettingsService::new(storage.system_settings_repo.clone()));
347 preload_settings_cache(&settings_service);
348
349 let encryption_service = EncryptionService::from_secret(&config.jwt.secret);
351 auto_generate_sidecar_secrets(&storage.system_settings_repo, &encryption_service);
352 preload_settings_cache(&settings_service);
354
355 let settings_service = Arc::new(SettingsService::with_encryption(
357 storage.system_settings_repo.clone(),
358 encryption_service.clone(),
359 ));
360 preload_settings_cache(&settings_service);
361
362 let jwt_service = JwtService::new(&config.jwt);
363 let password_service = PasswordService::default();
364 let google_service = GoogleService::new(&config.google);
365 let apple_service = AppleService::new(&config.apple);
366 let solana_service = SolanaService::new(&config.solana);
367 let totp_service = TotpService::new("Cedros");
368 let webauthn_service = WebAuthnService::new(&config.webauthn, settings_service.clone());
369 let audit_service = AuditService::new(storage.audit_repo.clone(), config.server.trust_proxy);
370 let step_up_service = StepUpService::new(storage.session_repo.clone());
371
372 let protocol = if config
375 .server
376 .frontend_url
377 .as_ref()
378 .map(|u| u.starts_with("https://"))
379 .unwrap_or(false)
380 {
381 "https"
382 } else {
383 "http"
384 };
385 let sso_callback_url = config.server.sso_callback_url.clone().unwrap_or_else(|| {
386 format!(
387 "{}://{}:{}/auth/sso/callback",
388 protocol, config.server.host, config.server.port
389 )
390 });
391 let oidc_service = OidcService::new(sso_callback_url);
392
393 let base_url = config
395 .server
396 .frontend_url
397 .clone()
398 .unwrap_or_else(|| "http://localhost:3000".to_string());
399 let token_cipher = TokenCipher::new(&config.jwt.secret);
400 let comms_service = CommsService::new(storage.outbox_repo.clone(), base_url, token_cipher);
401
402 let (privacy_sidecar_client, note_encryption_service) = if config.privacy.enabled {
405 let mut errors = Vec::new();
406
407 let resolved_api_key = config
409 .privacy
410 .sidecar_api_key
411 .clone()
412 .or_else(|| read_sidecar_secret_sync(&settings_service, "sidecar_api_key"));
413
414 let sidecar = match resolved_api_key {
415 Some(api_key) => match PrivacySidecarClient::new(SidecarClientConfig {
416 base_url: config.privacy.sidecar_url.clone(),
417 timeout_ms: config.privacy.sidecar_timeout_ms,
418 api_key,
419 }) {
420 Ok(s) => Some(Arc::new(s)),
421 Err(e) => {
422 errors.push(format!("Failed to create privacy sidecar client: {}", e));
423 None
424 }
425 },
426 None => {
427 errors.push("SIDECAR_API_KEY is required (env var or system_settings)".to_string());
428 None
429 }
430 };
431
432 let resolved_note_key = config
434 .privacy
435 .note_encryption_key
436 .clone()
437 .or_else(|| read_sidecar_secret_sync(&settings_service, "note_encryption_key"));
438
439 let note_encryption = match resolved_note_key.as_deref() {
440 Some(key) => match decode_note_encryption_key(key) {
441 Ok(key_bytes) => match build_note_encryption_service(
442 &key_bytes,
443 &config.privacy.note_encryption_key_id,
444 ) {
445 Ok(n) => Some(Arc::new(n)),
446 Err(e) => {
447 errors.push(format!("Failed to create note encryption service: {}", e));
448 None
449 }
450 },
451 Err(e) => {
452 errors.push(format!("Invalid base64 in note_encryption_key: {}", e));
453 None
454 }
455 },
456 None => {
457 errors.push("note_encryption_key is required when privacy is enabled (env var or system_settings)".to_string());
458 None
459 }
460 };
461
462 if !errors.is_empty() {
465 for error in &errors {
466 tracing::error!("{}", error);
467 }
468 panic!(
469 "Privacy is enabled but required services failed to initialize: {}",
470 errors.join("; ")
471 );
472 } else {
473 (sidecar, note_encryption)
474 }
475 } else {
476 (None, None)
477 };
478
479 let jupiter_swap_service = config
481 .privacy
482 .company_wallet_address
483 .as_ref()
484 .and_then(|wallet| {
485 match JupiterSwapService::new(
486 wallet.clone(),
487 &config.privacy.company_currency,
488 None, ) {
490 Ok(service) => Some(Arc::new(service)),
491 Err(e) => {
492 tracing::error!(error = %e, "Failed to create Jupiter swap service, swap features disabled");
493 None
494 }
495 }
496 });
497
498 let sol_price_service = Arc::new(SolPriceService::new());
500
501 let fee_service = Arc::new(DepositFeeService::new(settings_service.clone()));
503 let deposit_credit_service = Arc::new(DepositCreditService::new(
504 sol_price_service.clone(),
505 fee_service,
506 config.privacy.company_currency.clone(),
507 ));
508
509 let state = Arc::new(AppState {
510 config,
511 callback,
512 jwt_service,
513 password_service,
514 google_service,
515 apple_service,
516 solana_service,
517 totp_service,
518 webauthn_service,
519 oidc_service,
520 encryption_service,
521 phantom_email: std::marker::PhantomData::<LogEmailService>,
522 audit_service,
523 comms_service,
524 user_repo: storage.user_repo.clone(),
525 session_repo: storage.session_repo.clone(),
526 nonce_repo: storage.nonce_repo.clone(),
527 verification_repo: storage.verification_repo.clone(),
528 org_repo: storage.org_repo.clone(),
529 membership_repo: storage.membership_repo.clone(),
530 invite_repo: storage.invite_repo.clone(),
531 audit_repo: storage.audit_repo.clone(),
532 login_attempt_repo: storage.login_attempt_repo.clone(),
533 login_attempt_config: LoginAttemptConfig::default(),
534 totp_repo: storage.totp_repo.clone(),
535 custom_role_repo: storage.custom_role_repo.clone(),
536 policy_repo: storage.policy_repo.clone(),
537 outbox_repo: storage.outbox_repo.clone(),
538 api_key_repo: storage.api_key_repo.clone(),
539 wallet_material_repo: storage.wallet_material_repo.clone(),
540 derived_wallet_repo: storage.derived_wallet_repo.clone(),
541 wallet_rotation_history_repo: storage.wallet_rotation_history_repo.clone(),
542 credential_repo: storage.credential_repo.clone(),
543 webauthn_repo: storage.webauthn_repo.clone(),
544 deposit_repo: storage.deposit_repo.clone(),
545 credit_repo: storage.credit_repo.clone(),
546 credit_hold_repo: storage.credit_hold_repo.clone(),
547 credit_refund_request_repo: storage.credit_refund_request_repo.clone(),
548 privacy_note_repo: storage.privacy_note_repo.clone(),
549 system_settings_repo: storage.system_settings_repo.clone(),
550 treasury_config_repo: storage.treasury_config_repo.clone(),
551 user_withdrawal_log_repo: storage.user_withdrawal_log_repo.clone(),
552 referral_payout_repo: storage.referral_payout_repo.clone(),
553 referral_code_history_repo: storage.referral_code_history_repo.clone(),
554 settings_service: settings_service.clone(),
555 mfa_attempt_service: MfaAttemptService::new(),
556 step_up_service,
557 wallet_signing_service: WalletSigningService::new(),
558 wallet_unlock_cache: create_wallet_unlock_cache(),
559 privacy_sidecar_client,
560 note_encryption_service,
561 sol_price_service,
562 jupiter_swap_service,
563 deposit_credit_service,
564 kyc_service: Some(Arc::new(services::KycService::new(
565 storage.kyc_repo.clone(),
566 storage.user_repo.clone(),
567 settings_service.clone(),
568 ))),
569 accreditation_service: Some(Arc::new(services::AccreditationService::new(
570 storage.accreditation_repo.clone(),
571 storage.user_repo.clone(),
572 settings_service.clone(),
573 ))),
574 sanctions_service: Arc::new(SanctionsService::new(settings_service.clone())),
575 token_gating_service: Arc::new(TokenGatingService::new(
576 settings_service.clone(),
577 storage.user_repo.clone(),
578 storage.wallet_material_repo.clone(),
579 )),
580 signup_gating_service: Arc::new(SignupGatingService::new(
581 storage.access_code_repo.clone(),
582 storage.user_repo.clone(),
583 settings_service.clone(),
584 )),
585 #[cfg(feature = "postgres")]
586 postgres_pool: storage.pg_pool.clone(),
587 storage,
588 });
589 create_router(state)
590}
591
592pub fn create_withdrawal_worker(
600 config: &Config,
601 storage: &Storage,
602 settings_service: Arc<SettingsService>,
603 notification_service: Arc<dyn services::NotificationService>,
604 cancel_token: tokio_util::sync::CancellationToken,
605) -> Option<tokio::task::JoinHandle<()>> {
606 if !config.privacy.enabled {
607 return None;
608 }
609
610 let sidecar = match build_privacy_sidecar_client(config) {
612 Ok(s) => Arc::new(s),
613 Err(e) => {
614 tracing::error!(error = %e, "Failed to create privacy sidecar client for withdrawal worker");
615 return None;
616 }
617 };
618
619 let encryption_key = match config.privacy.note_encryption_key.as_ref() {
621 Some(k) => k,
622 None => {
623 tracing::error!("note_encryption_key is required when privacy is enabled");
624 return None;
625 }
626 };
627 let key_bytes = match decode_note_encryption_key(encryption_key) {
628 Ok(k) => k,
629 Err(e) => {
630 tracing::error!(error = %e, "Invalid base64 in note_encryption_key");
631 return None;
632 }
633 };
634 let note_encryption = match build_note_encryption_service(
635 &key_bytes,
636 &config.privacy.note_encryption_key_id,
637 ) {
638 Ok(s) => Arc::new(s),
639 Err(e) => {
640 tracing::error!(error = %e, "Failed to create note encryption service for withdrawal worker");
641 return None;
642 }
643 };
644
645 use services::{WithdrawalWorker, WithdrawalWorkerConfig};
648 let worker_config = WithdrawalWorkerConfig {
649 company_currency: config.privacy.company_currency.clone(),
650 };
651 let worker = WithdrawalWorker::new(
652 storage.deposit_repo.clone(),
653 storage.withdrawal_history_repo.clone(),
654 sidecar,
655 note_encryption,
656 notification_service,
657 settings_service,
658 worker_config,
659 );
660
661 Some(worker.start(cancel_token))
662}
663
664pub fn create_micro_batch_worker(
673 config: &Config,
674 storage: &Storage,
675 settings_service: Arc<SettingsService>,
676 cancel_token: tokio_util::sync::CancellationToken,
677) -> Option<tokio::task::JoinHandle<()>> {
678 if !config.privacy.enabled {
679 return None;
680 }
681
682 let sidecar = match build_privacy_sidecar_client(config) {
684 Ok(s) => Arc::new(s),
685 Err(e) => {
686 tracing::error!(error = %e, "Failed to create privacy sidecar client for micro batch worker");
687 return None;
688 }
689 };
690
691 let encryption_key = match config.privacy.note_encryption_key.as_ref() {
693 Some(k) => k,
694 None => {
695 tracing::error!("note_encryption_key is required when privacy is enabled");
696 return None;
697 }
698 };
699 let key_bytes = match decode_note_encryption_key(encryption_key) {
700 Ok(k) => k,
701 Err(e) => {
702 tracing::error!(error = %e, "Invalid base64 in note_encryption_key");
703 return None;
704 }
705 };
706 let note_encryption = match build_note_encryption_service(
707 &key_bytes,
708 &config.privacy.note_encryption_key_id,
709 ) {
710 Ok(s) => Arc::new(s),
711 Err(e) => {
712 tracing::error!(error = %e, "Failed to create note encryption service for micro batch worker");
713 return None;
714 }
715 };
716
717 let sol_price_service = Arc::new(services::SolPriceService::new());
719
720 use services::MicroBatchWorker;
722 let worker = MicroBatchWorker::new(
723 storage.deposit_repo.clone(),
724 storage.treasury_config_repo.clone(),
725 sidecar,
726 sol_price_service,
727 note_encryption,
728 settings_service,
729 config.privacy.company_currency.clone(),
730 );
731
732 Some(worker.start(cancel_token))
733}
734
735pub fn create_referral_payout_worker(
744 config: &Config,
745 storage: &Storage,
746 settings_service: Arc<SettingsService>,
747 cancel_token: tokio_util::sync::CancellationToken,
748) -> Option<tokio::task::JoinHandle<()>> {
749 if !config.privacy.enabled {
750 return None;
751 }
752
753 let sidecar = match build_privacy_sidecar_client(config) {
754 Ok(s) => Arc::new(s),
755 Err(e) => {
756 tracing::error!(error = %e, "Failed to create sidecar client for referral payout worker");
757 return None;
758 }
759 };
760
761 let encryption_key = match config.privacy.note_encryption_key.as_ref() {
762 Some(k) => k,
763 None => {
764 tracing::error!("note_encryption_key is required for referral payout worker");
765 return None;
766 }
767 };
768 let key_bytes = match decode_note_encryption_key(encryption_key) {
769 Ok(k) => k,
770 Err(e) => {
771 tracing::error!(error = %e, "Invalid base64 in note_encryption_key");
772 return None;
773 }
774 };
775 let note_encryption = match build_note_encryption_service(
776 &key_bytes,
777 &config.privacy.note_encryption_key_id,
778 ) {
779 Ok(s) => Arc::new(s),
780 Err(e) => {
781 tracing::error!(error = %e, "Failed to create note encryption for referral payout worker");
782 return None;
783 }
784 };
785
786 let worker = ReferralPayoutWorker::new(
787 storage.referral_payout_repo.clone(),
788 storage.treasury_config_repo.clone(),
789 sidecar,
790 note_encryption,
791 settings_service,
792 );
793
794 Some(worker.start(cancel_token))
795}
796
797pub fn create_hold_expiration_worker(
804 storage: &Storage,
805 cancel_token: tokio_util::sync::CancellationToken,
806) -> tokio::task::JoinHandle<()> {
807 use services::{HoldExpirationConfig, HoldExpirationWorker};
808
809 let worker = HoldExpirationWorker::new(
810 storage.credit_repo.clone(),
811 storage.credit_hold_repo.clone(),
812 HoldExpirationConfig::default(),
813 );
814
815 worker.start(cancel_token)
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821 use base64::Engine;
822
823 fn base_config() -> Config {
824 use crate::config::{
825 default_access_expiry, default_audience, default_issuer, default_refresh_expiry,
826 AppleConfig, CookieConfig, CorsConfig, DatabaseConfig, EmailConfig, GoogleConfig,
827 JwtConfig, NotificationConfig, PrivacyConfig, RateLimitConfig, ServerConfig,
828 SolanaConfig, SsoConfig, WalletConfig, WebAuthnConfig, WebhookConfig,
829 };
830
831 Config {
832 server: ServerConfig::default(),
833 jwt: JwtConfig {
834 secret: "s".repeat(32),
835 rsa_private_key_pem: None,
836 issuer: default_issuer(),
837 audience: default_audience(),
838 access_token_expiry: default_access_expiry(),
839 refresh_token_expiry: default_refresh_expiry(),
840 },
841 email: EmailConfig::default(),
842 google: GoogleConfig {
843 enabled: false,
844 client_id: None,
845 },
846 apple: AppleConfig {
847 enabled: false,
848 client_id: None,
849 team_id: None,
850 ..AppleConfig::default()
851 },
852 solana: SolanaConfig::default(),
853 webauthn: WebAuthnConfig::default(),
854 cors: CorsConfig::default(),
855 cookie: CookieConfig::default(),
856 webhook: WebhookConfig::default(),
857 rate_limit: RateLimitConfig::default(),
858 database: DatabaseConfig::default(),
859 notification: NotificationConfig::default(),
860 sso: SsoConfig::default(),
861 wallet: WalletConfig::default(),
862 privacy: PrivacyConfig::default(),
863 }
864 }
865
866 #[test]
867 fn test_decode_note_encryption_key_valid() {
868 let key = base64::engine::general_purpose::STANDARD.encode([0u8; 32]);
869 let bytes = decode_note_encryption_key(&key).expect("valid base64 should decode");
870 assert_eq!(bytes.len(), 32);
871 assert!(bytes.iter().all(|byte| *byte == 0));
872 }
873
874 #[test]
875 fn test_decode_note_encryption_key_invalid() {
876 assert!(decode_note_encryption_key("not-base64").is_err());
877 }
878
879 #[test]
880 fn test_build_privacy_sidecar_client_requires_api_key() {
881 let mut config = base_config();
882 config.privacy.enabled = true;
883 config.privacy.sidecar_api_key = None;
884
885 match build_privacy_sidecar_client(&config) {
886 Ok(_) => panic!("expected error for missing SIDECAR_API_KEY"),
887 Err(err) => {
888 assert!(err.to_string().contains("SIDECAR_API_KEY is required"));
889 }
890 }
891 }
892
893 #[test]
894 fn test_build_privacy_sidecar_client_with_api_key() {
895 let mut config = base_config();
896 config.privacy.enabled = true;
897 config.privacy.sidecar_api_key = Some("test-key".to_string());
898
899 assert!(build_privacy_sidecar_client(&config).is_ok());
900 }
901
902 #[test]
903 fn test_preload_settings_cache_populates_cached_values() {
904 let storage = Storage::in_memory();
905 let settings_service = Arc::new(SettingsService::new(storage.system_settings_repo));
906 let runtime = tokio::runtime::Builder::new_multi_thread()
907 .worker_threads(1)
908 .enable_all()
909 .build()
910 .expect("runtime");
911
912 runtime.block_on(async {
913 preload_settings_cache(&settings_service);
914 });
915
916 assert!(settings_service
917 .get_cached_u32_sync("rate_limit_auth")
918 .is_some());
919 }
920}