use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::callback::AuthCallback;
use crate::errors::AppError;
use crate::AppState;
use crate::models::AuthMethod;
use crate::repositories::{MembershipEntity, OrgEntity, OrgRole, UserEntity};
use crate::services::EmailService;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupStatusResponse {
pub needs_setup: bool,
pub has_admin: bool,
pub server_version: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateFirstAdminRequest {
pub email: String,
pub password: String,
pub name: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateFirstAdminResponse {
pub success: bool,
pub user_id: Uuid,
pub message: String,
}
pub async fn setup_status<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
) -> Result<impl IntoResponse, AppError> {
let has_admin = check_has_admin(&state).await?;
Ok(Json(SetupStatusResponse {
needs_setup: !has_admin,
has_admin,
server_version: env!("CARGO_PKG_VERSION").to_string(),
}))
}
pub async fn create_first_admin<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
Json(req): Json<CreateFirstAdminRequest>,
) -> Result<impl IntoResponse, AppError> {
let has_admin = check_has_admin(&state).await?;
if has_admin {
return Err(AppError::Forbidden(
"Setup already completed. An admin user already exists.".into(),
));
}
if !req.email.contains('@') || req.email.len() < 5 {
return Err(AppError::Validation("Invalid email format".into()));
}
if req.password.len() < 8 {
return Err(AppError::Validation(
"Password must be at least 8 characters".into(),
));
}
if let Some(_existing) = state.user_repo.find_by_email(&req.email).await? {
return Err(AppError::Validation("Email already registered".into()));
}
let password_hash = state.password_service.hash(req.password.clone()).await?;
let user_id = Uuid::new_v4();
let now = chrono::Utc::now();
let user = UserEntity {
id: user_id,
email: Some(req.email.clone()),
email_verified: true, password_hash: Some(password_hash),
name: req.name,
picture: None,
wallet_address: None,
google_id: None,
apple_id: None,
stripe_customer_id: None,
auth_methods: vec![AuthMethod::Email],
is_system_admin: true, created_at: now,
updated_at: now,
last_login_at: Some(now),
};
state.user_repo.create(user).await?;
let org_id = Uuid::new_v4();
let org = OrgEntity {
id: org_id,
name: "Personal".to_string(),
slug: format!("personal-{}", &user_id.to_string()[..8]),
logo_url: None,
is_personal: true,
owner_id: user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
state.org_repo.create(org).await?;
let membership_id = Uuid::new_v4();
let membership = MembershipEntity {
id: membership_id,
user_id,
org_id,
role: OrgRole::Owner,
joined_at: chrono::Utc::now(),
};
state.membership_repo.create(membership).await?;
tracing::info!(
user_id = %user_id,
email = %req.email,
"First admin user created during setup"
);
Ok((
StatusCode::CREATED,
Json(CreateFirstAdminResponse {
success: true,
user_id,
message: "Admin account created successfully. You can now log in.".to_string(),
}),
))
}
async fn check_has_admin<C: AuthCallback, E: EmailService>(
state: &Arc<AppState<C, E>>,
) -> Result<bool, AppError> {
let admin_count = state.user_repo.count_system_admins().await?;
Ok(admin_count > 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_setup_status_serialization() {
let status = SetupStatusResponse {
needs_setup: true,
has_admin: false,
server_version: "1.0.0".to_string(),
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("needsSetup"));
assert!(json.contains("hasAdmin"));
}
#[test]
fn test_create_admin_request_deserialization() {
let json = r#"{"email":"admin@example.com","password":"secret123","name":"Admin"}"#;
let req: CreateFirstAdminRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.email, "admin@example.com");
assert_eq!(req.password, "secret123");
assert_eq!(req.name, Some("Admin".to_string()));
}
}