feat: add React frontend with login, signup, dashboard pages and admin seeding support

This commit is contained in:
2025-12-19 17:33:23 +05:30
parent 2d3ad4f567
commit b2ad488043
26 changed files with 4302 additions and 15 deletions

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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(())
}