feat: add React frontend with login, signup, dashboard pages and admin seeding support
This commit is contained in:
@@ -19,6 +19,11 @@ auth:
|
||||
cookie_name: "zhealth_session"
|
||||
cookie_secure: false # Set to true in production with HTTPS
|
||||
|
||||
# Default admin user (created on first startup)
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "${ADMIN_PASSWORD}" # Use env var in production
|
||||
|
||||
ai:
|
||||
provider: "gemini" # Options: gemini | openai | anthropic
|
||||
model: "gemini-3-flash-preview"
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Config {
|
||||
pub paths: PathsConfig,
|
||||
pub logging: LoggingConfig,
|
||||
pub auth: AuthConfig,
|
||||
pub admin: AdminConfig,
|
||||
pub ai: AiConfig,
|
||||
}
|
||||
|
||||
@@ -38,6 +39,12 @@ pub struct AuthConfig {
|
||||
pub cookie_secure: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AdminConfig {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AiConfig {
|
||||
pub provider: String,
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::models::user::{diet, role, user};
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub role_name: Option<String>, // defaults to "user"
|
||||
pub height_cm: Option<f32>,
|
||||
@@ -29,7 +28,6 @@ pub struct CreateUserRequest {
|
||||
/// Request to update a user.
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUserRequest {
|
||||
pub email: Option<String>,
|
||||
pub height_cm: Option<f32>,
|
||||
pub blood_type: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
@@ -43,7 +41,6 @@ pub struct UpdateUserRequest {
|
||||
pub struct UserResponse {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub role: String,
|
||||
pub height_cm: Option<f32>,
|
||||
pub blood_type: Option<String>,
|
||||
@@ -88,7 +85,6 @@ pub async fn list_users(
|
||||
.map(|u| UserResponse {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
role: role_map.get(&u.role_id).cloned().unwrap_or_default(),
|
||||
height_cm: u.height_cm,
|
||||
blood_type: u.blood_type,
|
||||
@@ -134,7 +130,6 @@ pub async fn get_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
role: role_name,
|
||||
height_cm: u.height_cm,
|
||||
blood_type: u.blood_type,
|
||||
@@ -181,7 +176,6 @@ pub async fn create_user(
|
||||
|
||||
let new_user = user::ActiveModel {
|
||||
username: Set(req.username.clone()),
|
||||
email: Set(req.email.clone()),
|
||||
password_hash: Set(password_hash),
|
||||
role_id: Set(role_entity.id),
|
||||
height_cm: Set(req.height_cm),
|
||||
@@ -217,7 +211,6 @@ pub async fn create_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: inserted.id,
|
||||
username: inserted.username,
|
||||
email: inserted.email,
|
||||
role: role_name,
|
||||
height_cm: inserted.height_cm,
|
||||
blood_type: inserted.blood_type,
|
||||
@@ -253,9 +246,6 @@ pub async fn update_user(
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let mut active: user::ActiveModel = existing.into();
|
||||
if let Some(email) = req.email {
|
||||
active.email = Set(email);
|
||||
}
|
||||
if req.height_cm.is_some() {
|
||||
active.height_cm = Set(req.height_cm);
|
||||
}
|
||||
@@ -301,7 +291,6 @@ pub async fn update_user(
|
||||
Ok(Json(UserResponse {
|
||||
id: updated.id,
|
||||
username: updated.username,
|
||||
email: updated.email,
|
||||
role: role_name,
|
||||
height_cm: updated.height_cm,
|
||||
blood_type: updated.blood_type,
|
||||
|
||||
@@ -77,6 +77,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
let biomarker_data = seed::BiomarkerSeedData::load("seed_biomarkers.yaml")?;
|
||||
seed::sync_biomarker_data(&db, &biomarker_data).await?;
|
||||
|
||||
// Create admin user from config
|
||||
seed::sync_admin_user(&db, &config.admin).await?;
|
||||
|
||||
tracing::info!("Seed data synced.");
|
||||
|
||||
// Start server
|
||||
|
||||
@@ -15,9 +15,6 @@ pub struct Model {
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub password_hash: String,
|
||||
|
||||
#[sea_orm(unique)]
|
||||
pub email: String,
|
||||
|
||||
/// Foreign key to roles table
|
||||
pub role_id: i32,
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
//! Seed data loading and syncing.
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
|
||||
use serde::Deserialize;
|
||||
use serde_yaml::Value;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::config::AdminConfig;
|
||||
use crate::models::bio::{biomarker, biomarker_category, biomarker_reference_rule};
|
||||
use crate::models::user::{diet, role};
|
||||
use crate::models::user::{diet, role, user};
|
||||
|
||||
// ============================================================================
|
||||
// Seed Data Structures
|
||||
@@ -475,3 +477,44 @@ pub async fn sync_biomarker_data(db: &DatabaseConnection, seed: &BiomarkerSeedDa
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync admin user from config (create if not exists).
|
||||
pub async fn sync_admin_user(db: &DatabaseConnection, admin_config: &AdminConfig) -> anyhow::Result<()> {
|
||||
// Check if admin already exists
|
||||
let existing = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(&admin_config.username))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if existing.is_some() {
|
||||
tracing::debug!("Admin user already exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get admin role
|
||||
let admin_role = role::Entity::find()
|
||||
.filter(role::Column::Name.eq("admin"))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Admin role not found - run seed first"))?;
|
||||
|
||||
// Hash password
|
||||
let password_hash = crate::auth::hash_password(&admin_config.password)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to hash admin password: {:?}", e))?;
|
||||
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let new_admin = user::ActiveModel {
|
||||
username: Set(admin_config.username.clone()),
|
||||
password_hash: Set(password_hash),
|
||||
role_id: Set(admin_role.id),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
new_admin.insert(db).await?;
|
||||
tracing::info!("Created admin user: {}", admin_config.username);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user