use axum::extract::State;
use axum::Json;
use serde::Serialize;
use std::sync::Arc;
use crate::callback::AuthCallback;
use crate::services::EmailService;
use crate::AppState;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthFeaturesResponse {
pub email: bool,
pub google: bool,
pub apple: bool,
pub solana: bool,
pub webauthn: bool,
pub instant_link: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub google_client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub apple_client_id: Option<String>,
pub username_enabled: bool,
pub wallet_enroll_enabled: bool,
pub show_recovery_enabled: bool,
pub social_button_order: Vec<String>,
pub kyc_enabled: bool,
pub kyc_enforcement_mode: String,
pub accreditation_enabled: bool,
pub accreditation_enforcement_mode: String,
pub token_gating_enabled: bool,
pub signup_limit_enabled: bool,
pub signup_access_code_required: bool,
}
pub async fn auth_features<C: AuthCallback + 'static, E: EmailService + 'static>(
State(state): State<Arc<AppState<C, E>>>,
) -> Json<AuthFeaturesResponse> {
let ss = &state.settings_service;
let cfg = &state.config;
let email = ss
.get_bool("auth_email_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.email.enabled);
let google = ss
.get_bool("auth_google_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.google.enabled);
let apple = ss
.get_bool("auth_apple_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.apple.enabled);
let solana = ss
.get_bool("auth_solana_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.solana.enabled);
let webauthn = ss
.get_bool("auth_webauthn_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.webauthn.enabled);
let instant_link = ss
.get_bool("auth_instantlink_enabled")
.await
.ok()
.flatten()
.unwrap_or(cfg.email.enabled);
let google_client_id = if google {
ss.get("auth_google_client_id")
.await
.ok()
.flatten()
.filter(|s| !s.is_empty())
.or_else(|| cfg.google.client_id.clone())
} else {
None
};
let apple_client_id = if apple {
ss.get("auth_apple_client_id")
.await
.ok()
.flatten()
.filter(|s| !s.is_empty())
.or_else(|| cfg.apple.client_id.clone())
} else {
None
};
let username_enabled = ss
.get_bool("postlogin_username_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let wallet_enroll_enabled = ss
.get_bool("postlogin_wallet_enroll_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let show_recovery_enabled = ss
.get_bool("postlogin_show_recovery_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let default_order: Vec<String> = vec![
"webauthn".into(),
"google".into(),
"apple".into(),
"solana".into(),
];
let social_button_order = ss
.get("ui_social_button_order")
.await
.ok()
.flatten()
.filter(|s| !s.is_empty())
.map(|s| s.split(',').map(|p| p.trim().to_string()).collect::<Vec<_>>())
.unwrap_or(default_order);
let kyc_enabled = ss
.get_bool("kyc_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let kyc_enforcement_mode = ss
.get("kyc_enforcement_mode")
.await
.ok()
.flatten()
.unwrap_or_else(|| "none".to_string());
let accreditation_enabled = ss
.get_bool("accreditation_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let accreditation_enforcement_mode = ss
.get("accreditation_enforcement_mode")
.await
.ok()
.flatten()
.unwrap_or_else(|| "none".to_string());
let token_gating_enabled = ss
.get_bool("token_gating_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let signup_limit_enabled = ss
.get_bool("signup_limit_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
let signup_access_code_required = ss
.get_bool("signup_access_code_enabled")
.await
.ok()
.flatten()
.unwrap_or(false);
Json(AuthFeaturesResponse {
email,
google,
apple,
solana,
webauthn,
instant_link,
google_client_id,
apple_client_id,
username_enabled,
wallet_enroll_enabled,
show_recovery_enabled,
social_button_order,
kyc_enabled,
kyc_enforcement_mode,
accreditation_enabled,
accreditation_enforcement_mode,
token_gating_enabled,
signup_limit_enabled,
signup_access_code_required,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn response_serializes_as_camel_case() {
let resp = AuthFeaturesResponse {
email: true,
google: false,
apple: true,
solana: false,
webauthn: true,
instant_link: false,
google_client_id: None,
apple_client_id: None,
username_enabled: false,
wallet_enroll_enabled: false,
show_recovery_enabled: false,
social_button_order: vec!["webauthn".into(), "google".into()],
kyc_enabled: false,
kyc_enforcement_mode: "none".to_string(),
accreditation_enabled: false,
accreditation_enforcement_mode: "none".to_string(),
token_gating_enabled: false,
signup_limit_enabled: false,
signup_access_code_required: false,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"instantLink\":false"));
assert!(json.contains("\"webauthn\":true"));
assert!(!json.contains("instant_link"));
assert!(!json.contains("googleClientId"));
assert!(!json.contains("appleClientId"));
assert!(json.contains("\"socialButtonOrder\":[\"webauthn\",\"google\"]"));
assert!(json.contains("\"kycEnabled\":false"));
assert!(json.contains("\"kycEnforcementMode\":\"none\""));
assert!(json.contains("\"tokenGatingEnabled\":false"));
}
#[test]
fn response_includes_client_ids_when_present() {
let resp = AuthFeaturesResponse {
email: true,
google: true,
apple: true,
solana: false,
webauthn: false,
instant_link: false,
google_client_id: Some("goog-123.apps.googleusercontent.com".into()),
apple_client_id: Some("com.example.auth".into()),
username_enabled: false,
wallet_enroll_enabled: false,
show_recovery_enabled: false,
social_button_order: vec![
"webauthn".into(),
"google".into(),
"apple".into(),
"solana".into(),
],
kyc_enabled: false,
kyc_enforcement_mode: "none".to_string(),
accreditation_enabled: false,
accreditation_enforcement_mode: "none".to_string(),
token_gating_enabled: false,
signup_limit_enabled: false,
signup_access_code_required: false,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"googleClientId\":\"goog-123.apps.googleusercontent.com\""));
assert!(json.contains("\"appleClientId\":\"com.example.auth\""));
}
}