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:
2026-04-13 12:36:01 +02:00
parent 5b87f73a42
commit 44dac06961
6 changed files with 215 additions and 2 deletions
+1
View File
@@ -1,4 +1,5 @@
server/target/
.env
users.toml
*.snap.new
repomix-output.xml
+33
View File
@@ -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
+131
View File
@@ -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,
}
}
+28
View File
@@ -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
View File
@@ -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);
}
+11
View File
@@ -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"