use axum::{
extract::State,
http::{header, HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use chrono::{Duration, Utc};
use std::sync::Arc;
use crate::callback::{AuthCallback, AuthCallbackPayload};
use crate::errors::AppError;
use crate::models::{AuthMethod, AuthResponse, RegisterRequest};
use crate::repositories::{
default_expiry, generate_api_key, generate_verification_token, hash_verification_token,
normalize_email, validate_email_ascii_local, ApiKeyEntity, AuditEventType, MembershipEntity,
SessionEntity, TokenType, UserEntity,
};
use crate::services::{EmailService, TokenContext};
use crate::utils::{
attach_auth_cookies, compute_post_login, extract_client_ip_with_fallback, hash_refresh_token,
is_disposable_email, is_valid_email, resolve_org_assignment, user_entity_to_auth_user, PeerIp,
};
use crate::AppState;
pub async fn register<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
PeerIp(peer_ip): PeerIp,
Json(req): Json<RegisterRequest>,
) -> Result<impl IntoResponse, AppError> {
let email_enabled = state
.settings_service
.get_bool("auth_email_enabled")
.await
.ok()
.flatten()
.unwrap_or(state.config.email.enabled);
if !email_enabled {
return Err(AppError::NotFound("Email auth disabled".into()));
}
state.sanctions_service.check_country_from_request(&headers).await?;
if !is_valid_email(&req.email) {
return Err(AppError::Validation("Invalid email format".to_string()));
}
let block_disposable = state
.settings_service
.get_bool("auth_email_block_disposable")
.await
.ok()
.flatten()
.unwrap_or(state.config.email.block_disposable_emails);
if block_disposable {
let mut custom_domains: std::collections::HashSet<String> = state
.config
.email
.custom_blocked_domains
.iter()
.cloned()
.collect();
if let Ok(Some(db_domains)) = state.settings_service.get("custom_blocked_domains").await {
if let Ok(domains) = serde_json::from_str::<Vec<String>>(&db_domains) {
custom_domains.extend(domains.into_iter().map(|d| d.to_lowercase()));
}
}
let custom_ref = if custom_domains.is_empty() {
None
} else {
Some(&custom_domains)
};
if is_disposable_email(&req.email, custom_ref) {
return Err(AppError::DisposableEmailBlocked);
}
}
validate_email_ascii_local(&req.email)?;
let normalized_email = normalize_email(&req.email);
state.password_service.validate(&req.password)?;
let password_hash = state.password_service.hash(req.password.clone()).await?;
if state.user_repo.email_exists(&normalized_email).await? {
return Err(AppError::EmailExists);
}
let gate_result = state
.signup_gating_service
.check_signup(req.access_code.as_deref())
.await?;
let mut user =
UserEntity::new_email_user(normalized_email.clone(), password_hash, req.name.clone());
let referrals_enabled = state
.settings_service
.get_bool("feature_referrals_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
if referrals_enabled {
if let Some(ref code) = req.referral {
match state.user_repo.find_by_referral_code(code).await {
Ok(Some(referrer)) => {
user.referred_by = Some(referrer.id);
}
Ok(None) => {
tracing::debug!(referral_code = %code, "Referral code not found, ignoring");
}
Err(e) => {
tracing::warn!(error = %e, "Failed to look up referral code, ignoring");
}
}
}
}
if !state.config.email.require_verification {
user.email_verified = true;
}
let (raw_api_key, api_key_entity) = if state.config.email.require_verification {
(None, None)
} else {
let raw = generate_api_key();
(
Some(raw.clone()),
Some(ApiKeyEntity::new(user.id, &raw, "default")),
)
};
let ip_address =
extract_client_ip_with_fallback(&headers, state.config.server.trust_proxy, peer_ip);
let user_agent = headers
.get(header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
user = state.user_repo.create(user).await?;
let org_assignment = resolve_org_assignment(&state, user.id).await?;
let membership = MembershipEntity::new(user.id, org_assignment.org_id, org_assignment.role);
let session_id = uuid::Uuid::new_v4();
let token_context = TokenContext {
org_id: Some(org_assignment.org_id),
role: Some(org_assignment.role.as_str().to_string()),
is_system_admin: None,
email_verified: Some(user.email_verified),
};
let token_pair =
state
.jwt_service
.generate_token_pair_with_context(user.id, session_id, &token_context)?;
let refresh_expiry =
Utc::now() + Duration::seconds(state.jwt_service.refresh_expiry_secs() as i64);
let mut session = SessionEntity::new_with_id(
session_id,
user.id,
hash_refresh_token(&token_pair.refresh_token, &state.config.jwt.secret),
refresh_expiry,
ip_address.clone(),
user_agent.clone(),
);
session.last_strong_auth_at = Some(Utc::now());
state.membership_repo.create(membership).await?;
if let Some(api_key_entity) = api_key_entity {
state.api_key_repo.create(api_key_entity).await?;
}
state.session_repo.create(session).await?;
if let Some(code_id) = gate_result.access_code_id {
if let Err(e) = state.signup_gating_service.mark_code_used(code_id).await {
tracing::warn!(
user_id = %user.id,
code_id = %code_id,
error = %e,
"Failed to mark access code as used"
);
}
}
if let Err(e) = crate::utils::auto_enroll_wallet(&state, user.id, &req.password, &headers).await
{
tracing::warn!(user_id = %user.id, error = %e, "Auto wallet enrollment failed");
}
if let Some(referrer_id) = user.referred_by {
if let Err(e) = crate::services::referral_reward_service::issue_signup_reward(
&*state.user_repo,
&*state.credit_repo,
&*state.referral_payout_repo,
&state.settings_service,
&*state.callback,
user.id,
referrer_id,
&state.config.privacy.company_currency,
)
.await
{
tracing::warn!(
user_id = %user.id,
referrer_id = %referrer_id,
error = %e,
"Failed to issue referral signup reward"
);
}
}
let mut email_queued: Option<bool> = None;
if state.config.email.require_verification {
let token = generate_verification_token();
let token_hash = hash_verification_token(&token);
state
.verification_repo
.create(
user.id,
&token_hash,
TokenType::EmailVerify,
default_expiry(TokenType::EmailVerify),
)
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to create token: {}", e)))?;
let queued = state
.comms_service
.queue_verification_email(&req.email, user.name.as_deref(), &token, Some(user.id))
.await
.map_err(|e| {
tracing::warn!(
error = %e,
user_id = %user.id,
"Failed to queue verification email"
);
e
})
.is_ok();
email_queued = Some(queued);
}
let auth_user = user_entity_to_auth_user(&user);
let payload = AuthCallbackPayload {
user: auth_user.clone(),
method: AuthMethod::Email,
is_new_user: true,
session_id: session_id.to_string(),
ip_address,
user_agent,
referral: req.referral.clone(),
};
let callback_data =
super::call_registered_callback_with_timeout(&state.callback, &payload).await;
let _ = state
.audit_service
.log_user_event(AuditEventType::UserRegister, user.id, Some(&headers))
.await;
let response_tokens = if state.config.email.require_verification || state.config.cookie.enabled
{
None
} else {
Some(token_pair.clone())
};
let response = AuthResponse {
user: auth_user,
tokens: response_tokens,
is_new_user: true,
callback_data,
api_key: raw_api_key,
email_queued,
post_login: compute_post_login(&user, &state.settings_service, &*state.totp_repo, &*state.credential_repo, &*state.wallet_material_repo, &*state.storage.pending_wallet_recovery_repo).await,
};
let resp = (StatusCode::CREATED, Json(response)).into_response();
if state.config.email.require_verification {
Ok(resp)
} else {
Ok(attach_auth_cookies(
&state.config.cookie,
&token_pair,
state.jwt_service.refresh_expiry_secs(),
resp,
))
}
}