From 30f50e5b31294abd75c4b629970ad4865108738d Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Mon, 21 Jul 2025 20:37:16 +0300 Subject: feat: add cli option to create user --- src/main.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 139 insertions(+), 43 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 2e4dc75..4e89234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,18 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{post, put}; use axum::{Router, routing::get}; +use clap::{Parser, Subcommand}; use serde_json::json; use sqlx::SqlitePool; +use std::env; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::fs; +use tracing::{error, info}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use uuid::Uuid; mod auth; mod health; @@ -894,55 +902,143 @@ pub fn app_with_state(pool: std::sync::Arc) -> Router { .with_state(pool) } -use std::env; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::fs; -use uuid::Uuid; +#[derive(Parser)] +#[command(name = "silmataivas")] +struct Cli { + /// Increase output verbosity (-v, -vv, -vvv) + #[arg(short, long, action = clap::ArgAction::Count, global = true)] + verbose: u8, + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Start the Silmataivas server (default) + Server, + /// Create a new user with optional UUID + CreateUser { uuid: Option }, +} #[tokio::main] -async fn main() { - // Set up database path - let db_path = - env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://./data/silmataivas.db".to_string()); - // Ensure data directory exists - let _ = fs::create_dir_all("./data").await; - // Connect to SQLite - let pool = SqlitePool::connect(&db_path) - .await - .expect("Failed to connect to DB"); - - // Create initial admin user if none exists - { - let repo = users::UserRepository { db: &pool }; - match repo.any_admin_exists().await { - Ok(false) => { - let admin_token = - env::var("ADMIN_TOKEN").unwrap_or_else(|_| Uuid::new_v4().to_string()); - match repo - .create_user(Some(admin_token.clone()), Some(users::UserRole::Admin)) - .await - { - Ok(_) => println!("Initial admin user created. Token: {admin_token}"), - Err(e) => eprintln!("Failed to create initial admin user: {e}"), +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + // Set up logging based on verbosity + let filter = match cli.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + }; + let subscriber = FmtSubscriber::builder() + .with_env_filter(EnvFilter::new(filter)) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + match cli.command.unwrap_or(Commands::Server) { + Commands::Server => { + // Set up database path + let db_path = env::var("DATABASE_URL") + .unwrap_or_else(|_| "sqlite://./data/silmataivas.db".to_string()); + // Ensure data directory exists + let _ = fs::create_dir_all("./data").await; + // Connect to SQLite + let pool = SqlitePool::connect(&db_path) + .await + .expect("Failed to connect to DB"); + + // Create initial admin user if none exists + { + let repo = users::UserRepository { db: &pool }; + match repo.any_admin_exists().await { + Ok(false) => { + let admin_token = + env::var("ADMIN_TOKEN").unwrap_or_else(|_| Uuid::new_v4().to_string()); + match repo + .create_user(Some(admin_token.clone()), Some(users::UserRole::Admin)) + .await + { + Ok(_) => info!("Initial admin user created. Token: {}", admin_token), + Err(e) => error!("Failed to create initial admin user: {}", e), + } + } + Ok(true) => { + // At least one admin exists, do nothing + } + Err(e) => { + error!("Failed to check for existing admin users: {}", e); + } } } - Ok(true) => { - // At least one admin exists, do nothing - } - Err(e) => { - eprintln!("Failed to check for existing admin users: {e}"); - } + + let app = app_with_state(Arc::new(pool)); + let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); + let listener = tokio::net::TcpListener::bind(addr) + .await + .expect("Failed to bind address"); + info!("Listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await.unwrap(); } - } + Commands::CreateUser { uuid } => { + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = SqlitePool::connect(&db_url).await?; + let repo = crate::users::UserRepository { db: &pool }; + let user_id = uuid.unwrap_or_else(|| Uuid::new_v4().to_string()); + let user = repo + .create_user(Some(user_id.clone()), Some(crate::users::UserRole::User)) + .await?; - let app = app_with_state(Arc::new(pool)); - let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); - let listener = tokio::net::TcpListener::bind(addr) - .await - .expect("Failed to bind address"); - println!("Listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); + // Set up default NTFY settings + let ntfy_repo = crate::notifications::NtfySettingsRepository { db: &pool }; + ntfy_repo + .create(crate::notifications::NtfySettingsInput { + user_id: user.id, + enabled: true, + topic: user_id.clone(), + server_url: "https://ntfy.sh".to_string(), + priority: 3, + title_template: None, + message_template: None, + }) + .await?; + + // Set up default weather thresholds + let threshold_repo = + crate::weather_thresholds::WeatherThresholdRepository { db: &pool }; + threshold_repo + .create_threshold( + user.id, + "temperature".to_string(), + 35.0, + ">".to_string(), + true, + Some("Default: temperature > 35°C".to_string()), + ) + .await?; + threshold_repo + .create_threshold( + user.id, + "rain".to_string(), + 50.0, + ">".to_string(), + true, + Some("Default: rain > 50mm".to_string()), + ) + .await?; + threshold_repo + .create_threshold( + user.id, + "wind_speed".to_string(), + 80.0, + ">".to_string(), + true, + Some("Default: wind speed > 80km/h".to_string()), + ) + .await?; + + info!("User {} created", user_id); + } + } + Ok(()) } #[cfg(test)] -- cgit v1.2.3