Add configuration loading and error handling
Introduces a new `config` module for loading application settings from environment variables and a `users.toml` file. An `error` module is added to handle application-specific errors gracefully. An `.env.example` file is created to show available configuration options, and `users.toml.example` provides a template for user data. The main function now initializes the configuration and prints the server port.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
server/target/
|
||||
.env
|
||||
users.toml
|
||||
*.snap.new
|
||||
repomix-output.xml
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# === Required (Phase 1) ===
|
||||
|
||||
SERVER_PORT=3000
|
||||
DATA_PATH=/data
|
||||
USERS_FILE=users.toml
|
||||
LOG_LEVEL=info
|
||||
LOG_PATH=/var/log/recorder
|
||||
LOG_MAX_DAYS=90
|
||||
|
||||
# === Optional (Phase 2+) ===
|
||||
|
||||
# Retention
|
||||
RETENTION_AUDIO_DAYS=30
|
||||
RETENTION_TRANSCRIPT_DAYS=30
|
||||
RETENTION_DOCUMENT_DAYS=0
|
||||
|
||||
# faster-whisper
|
||||
WHISPER_URL=http://localhost:10300
|
||||
WHISPER_TIMEOUT_SECONDS=120
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=gemma3:4b
|
||||
OLLAMA_KEEP_ALIVE=0
|
||||
|
||||
# LLM provider (Ionos)
|
||||
LLM_URL=https://openai.ionos.com/openai
|
||||
LLM_API_KEY=
|
||||
LLM_MODEL=
|
||||
LLM_TEMPERATURE=0
|
||||
|
||||
# Session
|
||||
SESSION_TIMEOUT_HOURS=8
|
||||
@@ -0,0 +1,131 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A single user entry from users.toml.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
pub slug: String,
|
||||
pub api_key: String,
|
||||
pub web_password: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// Wrapper for deserializing the [[user]] array from TOML.
|
||||
#[derive(Deserialize)]
|
||||
struct UsersFile {
|
||||
user: Vec<User>,
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
// Phase 1 — required
|
||||
pub server_port: u16,
|
||||
pub data_path: PathBuf,
|
||||
pub log_level: String,
|
||||
pub log_path: PathBuf,
|
||||
pub log_max_days: u32,
|
||||
pub users: Vec<User>,
|
||||
pub api_keys: HashMap<String, String>, // api_key_value → slug
|
||||
|
||||
// Phase 2+ — optional with defaults
|
||||
pub retention_audio_days: u32,
|
||||
pub retention_transcript_days: u32,
|
||||
pub retention_document_days: u32,
|
||||
pub whisper_url: String,
|
||||
pub whisper_timeout_seconds: u64,
|
||||
pub ollama_url: String,
|
||||
pub ollama_model: String,
|
||||
pub ollama_keep_alive: u32,
|
||||
pub llm_url: String,
|
||||
pub llm_api_key: String,
|
||||
pub llm_model: String,
|
||||
pub llm_temperature: f32,
|
||||
pub session_timeout_hours: u32,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Self {
|
||||
let users_file = required_env("USERS_FILE");
|
||||
let (users, api_keys) = load_users(&users_file);
|
||||
|
||||
Self {
|
||||
server_port: required_env_parsed("SERVER_PORT"),
|
||||
data_path: PathBuf::from(required_env("DATA_PATH")),
|
||||
log_level: optional_env("LOG_LEVEL", "info"),
|
||||
log_path: PathBuf::from(optional_env("LOG_PATH", "/var/log/recorder")),
|
||||
log_max_days: optional_env_parsed("LOG_MAX_DAYS", 90),
|
||||
users,
|
||||
api_keys,
|
||||
|
||||
retention_audio_days: optional_env_parsed("RETENTION_AUDIO_DAYS", 30),
|
||||
retention_transcript_days: optional_env_parsed("RETENTION_TRANSCRIPT_DAYS", 30),
|
||||
retention_document_days: optional_env_parsed("RETENTION_DOCUMENT_DAYS", 0),
|
||||
whisper_url: optional_env("WHISPER_URL", "http://localhost:10300"),
|
||||
whisper_timeout_seconds: optional_env_parsed("WHISPER_TIMEOUT_SECONDS", 120),
|
||||
ollama_url: optional_env("OLLAMA_URL", "http://localhost:11434"),
|
||||
ollama_model: optional_env("OLLAMA_MODEL", "gemma3:4b"),
|
||||
ollama_keep_alive: optional_env_parsed("OLLAMA_KEEP_ALIVE", 0),
|
||||
llm_url: optional_env("LLM_URL", ""),
|
||||
llm_api_key: optional_env("LLM_API_KEY", ""),
|
||||
llm_model: optional_env("LLM_MODEL", ""),
|
||||
llm_temperature: optional_env_parsed("LLM_TEMPERATURE", 0.0),
|
||||
session_timeout_hours: optional_env_parsed("SESSION_TIMEOUT_HOURS", 8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load users from a TOML file and build the API key lookup map.
|
||||
fn load_users(path: &str) -> (Vec<User>, HashMap<String, String>) {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read users file {path}: {e}"));
|
||||
|
||||
let parsed: UsersFile = toml::from_str(&content)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse users file {path}: {e}"));
|
||||
|
||||
if parsed.user.is_empty() {
|
||||
panic!("No users defined in {path}. At least one is required.");
|
||||
}
|
||||
|
||||
let api_keys = parsed
|
||||
.user
|
||||
.iter()
|
||||
.map(|u| (u.api_key.clone(), u.slug.clone()))
|
||||
.collect();
|
||||
|
||||
(parsed.user, api_keys)
|
||||
}
|
||||
|
||||
/// Read a required environment variable. Panics with a clear message if missing.
|
||||
fn required_env(name: &str) -> String {
|
||||
std::env::var(name)
|
||||
.unwrap_or_else(|_| panic!("Missing required environment variable: {name}"))
|
||||
}
|
||||
|
||||
/// Read a required environment variable and parse it. Panics if missing or unparseable.
|
||||
fn required_env_parsed<T: std::str::FromStr>(name: &str) -> T
|
||||
where
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
let raw = required_env(name);
|
||||
raw.parse()
|
||||
.unwrap_or_else(|e| panic!("Invalid value for {name}: {e}"))
|
||||
}
|
||||
|
||||
/// Read an optional environment variable, falling back to a default.
|
||||
fn optional_env(name: &str, default: &str) -> String {
|
||||
std::env::var(name).unwrap_or_else(|_| default.to_owned())
|
||||
}
|
||||
|
||||
/// Read an optional environment variable and parse it, falling back to a default.
|
||||
fn optional_env_parsed<T: std::str::FromStr>(name: &str, default: T) -> T
|
||||
where
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
match std::env::var(name) {
|
||||
Ok(raw) => raw
|
||||
.parse()
|
||||
.unwrap_or_else(|e| panic!("Invalid value for {name}: {e}")),
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde_json::json;
|
||||
|
||||
pub enum AppError {
|
||||
Unauthorized,
|
||||
BadRequest(String),
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self {
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()),
|
||||
Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||
Self::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
|
||||
};
|
||||
|
||||
let body = axum::Json(json!({ "error": message }));
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::Internal(err.to_string())
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -1,3 +1,12 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
use config::Config;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
let config = Arc::new(Config::from_env());
|
||||
println!("Server starting on port {}", config.server_port);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[[user]]
|
||||
slug = "dr_mueller"
|
||||
api_key = "change-me-to-a-secure-key"
|
||||
web_password = "$2b$12$..."
|
||||
role = "doctor"
|
||||
|
||||
[[user]]
|
||||
slug = "dr_schmidt"
|
||||
api_key = "change-me-to-a-secure-key"
|
||||
web_password = "$2b$12$..."
|
||||
role = "doctor"
|
||||
Reference in New Issue
Block a user