init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

532
src/config/builder.rs Normal file
View File

@@ -0,0 +1,532 @@
use std::env;
use serde::{Deserialize, Serialize};
/// Centralized configuration builder for all environment variables and app settings
///
/// This builder consolidates all scattered env::var() calls throughout the codebase
/// into a single source of truth, following the established builder pattern architecture.
///
/// Usage:
/// ```rust,ignore
/// let config = ConfigurationBuilder::new().build();
/// if config.is_gitea_enabled() {
/// // Handle Gitea flow
/// }
/// ```
#[derive(Debug, Clone)]
pub struct ConfigurationBuilder {
// OAuth Configuration
gitea_client_id: Option<String>,
gitea_client_secret: Option<String>,
gitea_instance_url: Option<String>,
// App Configuration
app_url: String,
jwt_secret: String,
secret_key: Option<String>,
app_config_path: Option<String>,
/// Whether mock data/services are enabled (dev/test default: true; prod default: false)
enable_mock_data: bool,
/// Data source for marketplace data (fixtures/mock/live)
data_source: DataSource,
/// Filesystem path to fixtures (used when data_source=fixtures)
fixtures_path: String,
/// Whether demo mode UX/guards are enabled
demo_mode: bool,
/// Whether to enable in-memory catalog cache (dev/test only by default)
catalog_cache_enabled: bool,
/// TTL for catalog cache in seconds
catalog_cache_ttl_secs: u64,
// Server Configuration
environment: AppEnvironment,
}
/// Application environment types
#[derive(Debug, Clone, PartialEq)]
pub enum AppEnvironment {
Development,
Production,
Testing,
}
/// Data source for marketplace data
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DataSource {
/// Legacy in-memory mocks (dev only)
Mock,
/// Filesystem-backed fixtures under fixtures_path
Fixtures,
/// Live backend (e.g., PostgREST/DB)
Live,
}
/// Built configuration ready for use throughout the application
#[derive(Debug, Clone)]
pub struct AppConfiguration {
// OAuth Configuration
pub gitea_client_id: Option<String>,
pub gitea_client_secret: Option<String>,
pub gitea_instance_url: Option<String>,
// App Configuration
pub app_url: String,
pub jwt_secret: String,
pub secret_key: Option<String>,
pub app_config_path: Option<String>,
/// Whether mock data/services are enabled
pub enable_mock_data: bool,
/// Selected data source
pub data_source: DataSource,
/// Fixtures path when using fixtures
pub fixtures_path: String,
/// Demo mode enabled
pub demo_mode: bool,
/// Catalog cache enabled
pub catalog_cache_enabled: bool,
/// Catalog cache TTL in seconds
pub catalog_cache_ttl_secs: u64,
// Server Configuration
pub environment: AppEnvironment,
}
impl ConfigurationBuilder {
/// Creates a new configuration builder with default values
pub fn new() -> Self {
Self {
gitea_client_id: None,
gitea_client_secret: None,
gitea_instance_url: None,
app_url: "http://localhost:9999".to_string(),
jwt_secret: "your_jwt_secret_key".to_string(),
secret_key: None,
app_config_path: None,
enable_mock_data: true, // default true for development
data_source: DataSource::Fixtures,
fixtures_path: "./user_data".to_string(),
demo_mode: false,
catalog_cache_enabled: true,
catalog_cache_ttl_secs: 5,
environment: AppEnvironment::Development,
}
}
/// Creates a development configuration with sensible defaults
pub fn development() -> Self {
Self::new()
.environment(AppEnvironment::Development)
.app_url("http://localhost:9999".to_string())
.jwt_secret("dev_jwt_secret_key".to_string())
}
/// Creates a production configuration
pub fn production() -> Self {
Self::new()
.environment(AppEnvironment::Production)
.load_from_environment()
}
/// Creates a testing configuration
pub fn testing() -> Self {
Self::new()
.environment(AppEnvironment::Testing)
.app_url("http://localhost:8080".to_string())
.jwt_secret("test_jwt_secret_key".to_string())
}
/// Loads all configuration from environment variables
pub fn load_from_environment(mut self) -> Self {
// OAuth Configuration
self.gitea_client_id = env::var("GITEA_CLIENT_ID").ok().filter(|s| !s.is_empty());
self.gitea_client_secret = env::var("GITEA_CLIENT_SECRET").ok().filter(|s| !s.is_empty());
self.gitea_instance_url = env::var("GITEA_INSTANCE_URL").ok().filter(|s| !s.is_empty());
// App Configuration
if let Ok(app_url) = env::var("APP_URL") {
self.app_url = app_url;
}
if let Ok(jwt_secret) = env::var("JWT_SECRET") {
self.jwt_secret = jwt_secret;
}
self.secret_key = env::var("SECRET_KEY").ok().filter(|s| !s.is_empty());
self.app_config_path = env::var("APP_CONFIG").ok().filter(|s| !s.is_empty());
// Mock data gating (APP_ENABLE_MOCKS)
let enable_mocks_env = env::var("APP_ENABLE_MOCKS").ok();
if let Some(val) = enable_mocks_env.as_deref() {
let v = val.to_lowercase();
// Accept common truthy/falsey values
self.enable_mock_data = matches!(
v.as_str(),
"1" | "true" | "yes" | "on"
) || (!matches!(v.as_str(), "0" | "false" | "no" | "off") && v == "true");
}
// Catalog cache (APP_CATALOG_CACHE, APP_CATALOG_CACHE_TTL_SECS)
let catalog_cache_env = env::var("APP_CATALOG_CACHE").ok();
if let Some(val) = catalog_cache_env.as_deref() {
let v = val.to_lowercase();
self.catalog_cache_enabled = matches!(
v.as_str(),
"1" | "true" | "yes" | "on"
);
}
if let Ok(val) = env::var("APP_CATALOG_CACHE_TTL_SECS") {
if let Ok(parsed) = val.parse::<u64>() {
self.catalog_cache_ttl_secs = parsed;
}
}
// (no dev-specific cache flags; use APP_CATALOG_CACHE* across environments)
// Environment detection
if let Ok(env_var) = env::var("APP_ENV") {
self.environment = match env_var.to_lowercase().as_str() {
"production" | "prod" => AppEnvironment::Production,
"testing" | "test" => AppEnvironment::Testing,
_ => AppEnvironment::Development,
};
}
// Data source selection
if let Ok(ds) = env::var("APP_DATA_SOURCE") {
self.data_source = match ds.to_lowercase().as_str() {
"mock" => DataSource::Mock,
"live" => DataSource::Live,
_ => DataSource::Fixtures,
};
} else {
// Default by environment
self.data_source = match self.environment {
AppEnvironment::Production => DataSource::Live,
_ => DataSource::Fixtures,
};
}
// Fixtures path
if let Ok(path) = env::var("APP_FIXTURES_PATH") {
if !path.is_empty() {
self.fixtures_path = path;
}
}
// Demo mode
if let Ok(val) = env::var("APP_DEMO_MODE") {
let v = val.to_lowercase();
self.demo_mode = matches!(v.as_str(), "1" | "true" | "yes" | "on");
}
// If environment is production and APP_ENABLE_MOCKS not explicitly set,
// default to false to ensure clean production runtime
if self.environment == AppEnvironment::Production && enable_mocks_env.is_none() {
self.enable_mock_data = false;
}
// In production, disable catalog cache by default unless explicitly enabled
if self.environment == AppEnvironment::Production && catalog_cache_env.is_none() {
self.catalog_cache_enabled = false;
}
self
}
// Builder methods for fluent interface
pub fn gitea_client_id(mut self, client_id: String) -> Self {
self.gitea_client_id = Some(client_id);
self
}
pub fn gitea_client_secret(mut self, client_secret: String) -> Self {
self.gitea_client_secret = Some(client_secret);
self
}
pub fn gitea_instance_url(mut self, instance_url: String) -> Self {
self.gitea_instance_url = Some(instance_url);
self
}
pub fn app_url(mut self, url: String) -> Self {
self.app_url = url;
self
}
pub fn jwt_secret(mut self, secret: String) -> Self {
self.jwt_secret = secret;
self
}
pub fn secret_key(mut self, key: String) -> Self {
self.secret_key = Some(key);
self
}
pub fn app_config_path(mut self, path: String) -> Self {
self.app_config_path = Some(path);
self
}
pub fn environment(mut self, env: AppEnvironment) -> Self {
self.environment = env;
self
}
/// Builds the final configuration
pub fn build(self) -> AppConfiguration {
AppConfiguration {
gitea_client_id: self.gitea_client_id,
gitea_client_secret: self.gitea_client_secret,
gitea_instance_url: self.gitea_instance_url,
app_url: self.app_url,
jwt_secret: self.jwt_secret,
secret_key: self.secret_key,
app_config_path: self.app_config_path,
enable_mock_data: self.enable_mock_data,
data_source: self.data_source,
fixtures_path: self.fixtures_path,
demo_mode: self.demo_mode,
catalog_cache_enabled: self.catalog_cache_enabled,
catalog_cache_ttl_secs: self.catalog_cache_ttl_secs,
environment: self.environment,
}
}
}
impl AppConfiguration {
/// Convenience method to check if Gitea OAuth is enabled
pub fn is_gitea_enabled(&self) -> bool {
self.gitea_client_id.is_some() &&
self.gitea_client_secret.is_some() &&
self.gitea_instance_url.is_some()
}
/// Gets the Gitea client ID if available
pub fn gitea_client_id(&self) -> Option<&str> {
self.gitea_client_id.as_deref()
}
/// Gets the Gitea client secret if available
pub fn gitea_client_secret(&self) -> Option<&str> {
self.gitea_client_secret.as_deref()
}
/// Gets the Gitea instance URL if available
pub fn gitea_instance_url(&self) -> Option<&str> {
self.gitea_instance_url.as_deref()
}
/// Gets the app URL
pub fn app_url(&self) -> &str {
&self.app_url
}
/// Gets the JWT secret
pub fn jwt_secret(&self) -> &str {
&self.jwt_secret
}
/// Gets the secret key if available
pub fn secret_key(&self) -> Option<&str> {
self.secret_key.as_deref()
}
/// Gets the app config path if available
pub fn app_config_path(&self) -> Option<&str> {
self.app_config_path.as_deref()
}
/// Returns true if mock data/services are enabled
pub fn enable_mock_data(&self) -> bool {
self.enable_mock_data
}
/// Returns the configured data source
pub fn data_source(&self) -> &DataSource {
&self.data_source
}
/// True when using fixtures-backed data
pub fn is_fixtures(&self) -> bool {
matches!(self.data_source, DataSource::Fixtures)
}
/// True when using live backend
pub fn is_live(&self) -> bool {
matches!(self.data_source, DataSource::Live)
}
/// Path to fixtures directory
pub fn fixtures_path(&self) -> &str {
&self.fixtures_path
}
/// Demo mode flag
pub fn is_demo_mode(&self) -> bool {
self.demo_mode
}
/// Catalog cache enabled flag
pub fn is_catalog_cache_enabled(&self) -> bool {
self.catalog_cache_enabled
}
/// Catalog cache TTL (seconds)
pub fn catalog_cache_ttl_secs(&self) -> u64 {
self.catalog_cache_ttl_secs
}
/// Checks if running in development environment
pub fn is_development(&self) -> bool {
self.environment == AppEnvironment::Development
}
/// Checks if running in production environment
pub fn is_production(&self) -> bool {
self.environment == AppEnvironment::Production
}
/// Checks if running in testing environment
pub fn is_testing(&self) -> bool {
self.environment == AppEnvironment::Testing
}
}
impl Default for ConfigurationBuilder {
fn default() -> Self {
Self::new().load_from_environment()
}
}
/// Global configuration instance - lazy static for single initialization
use std::sync::OnceLock;
static GLOBAL_CONFIG: OnceLock<AppConfiguration> = OnceLock::new();
/// Gets the global application configuration
///
/// This function provides a singleton pattern for configuration access,
/// ensuring consistent configuration throughout the application lifecycle.
pub fn get_app_config() -> &'static AppConfiguration {
GLOBAL_CONFIG.get_or_init(|| {
ConfigurationBuilder::default().build()
})
}
/// Initializes the global configuration with a custom builder
///
/// This should be called once at application startup if custom configuration is needed.
/// If not called, the default environment-based configuration will be used.
pub fn init_app_config(config: AppConfiguration) -> Result<(), AppConfiguration> {
GLOBAL_CONFIG.set(config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{OnceLock, Mutex, MutexGuard};
use std::env;
// Serialize env-manipulating tests to avoid races
static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn env_lock() -> MutexGuard<'static, ()> {
ENV_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap()
}
#[test]
fn test_configuration_builder_defaults() {
let config = ConfigurationBuilder::new().build();
assert_eq!(config.app_url(), "http://localhost:9999");
assert_eq!(config.jwt_secret(), "your_jwt_secret_key");
assert!(!config.is_gitea_enabled());
}
#[test]
fn test_development_configuration() {
let config = ConfigurationBuilder::development().build();
assert!(config.is_development());
assert_eq!(config.app_url(), "http://localhost:9999");
}
#[test]
fn test_production_configuration() {
let config = ConfigurationBuilder::production().build();
assert!(config.is_production());
}
#[test]
fn test_gitea_enabled_check() {
let config = ConfigurationBuilder::new()
.gitea_client_id("test_id".to_string())
.gitea_client_secret("test_secret".to_string())
.gitea_instance_url("https://gitea.example.com".to_string())
.build();
assert!(config.is_gitea_enabled());
assert_eq!(config.gitea_client_id(), Some("test_id"));
}
#[test]
fn test_fluent_interface() {
let config = ConfigurationBuilder::new()
.app_url("https://example.com".to_string())
.jwt_secret("custom_secret".to_string())
.environment(AppEnvironment::Testing)
.build();
assert_eq!(config.app_url(), "https://example.com");
assert_eq!(config.jwt_secret(), "custom_secret");
assert!(config.is_testing());
}
#[test]
fn test_app_enable_mocks_truthy_values() {
let _g = env_lock();
// Ensure clean slate
env::remove_var("APP_ENV");
for val in ["1", "true", "yes", "on", "TRUE", "On"] {
env::set_var("APP_ENABLE_MOCKS", val);
let cfg = ConfigurationBuilder::default().build();
assert!(cfg.enable_mock_data(), "APP_ENABLE_MOCKS='{}' should enable mocks", val);
}
env::remove_var("APP_ENABLE_MOCKS");
}
#[test]
fn test_app_enable_mocks_falsey_values() {
let _g = env_lock();
env::remove_var("APP_ENV");
for val in ["0", "false", "no", "off", "FALSE", "Off"] {
env::set_var("APP_ENABLE_MOCKS", val);
let cfg = ConfigurationBuilder::default().build();
assert!(!cfg.enable_mock_data(), "APP_ENABLE_MOCKS='{}' should disable mocks", val);
}
env::remove_var("APP_ENABLE_MOCKS");
}
#[test]
fn test_production_default_disables_mocks_when_unset() {
let _g = env_lock();
env::set_var("APP_ENV", "production");
env::remove_var("APP_ENABLE_MOCKS");
let cfg = ConfigurationBuilder::default().build();
assert!(cfg.is_production());
assert!(!cfg.enable_mock_data(), "Production default should disable mocks when not explicitly set");
env::remove_var("APP_ENV");
}
#[test]
fn test_development_default_enables_mocks_when_unset() {
let _g = env_lock();
env::set_var("APP_ENV", "development");
env::remove_var("APP_ENABLE_MOCKS");
let cfg = ConfigurationBuilder::default().build();
assert!(cfg.is_development());
assert!(cfg.enable_mock_data(), "Development default should enable mocks when not explicitly set");
env::remove_var("APP_ENV");
}
}

72
src/config/mod.rs Normal file
View File

@@ -0,0 +1,72 @@
use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::env;
// Export OAuth module
pub mod oauth;
// Export configuration builder
pub mod builder;
pub use builder::get_app_config;
/// Application configuration
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
/// Server configuration
pub server: ServerConfig,
/// Template configuration
pub templates: TemplateConfig,
}
/// Server configuration
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
/// Host address to bind to
pub host: String,
/// Port to listen on
pub port: u16,
/// Workers count
pub workers: Option<u32>,
}
/// Template configuration
#[derive(Debug, Deserialize, Clone)]
pub struct TemplateConfig {
/// Directory containing templates
pub dir: String,
}
impl AppConfig {
/// Loads configuration from files and environment variables
pub fn new() -> Result<Self, ConfigError> {
// Set default values
let mut config_builder = Config::builder()
.set_default("server.host", "127.0.0.1")?
.set_default("server.port", 9999)?
.set_default("server.workers", None::<u32>)?
.set_default("templates.dir", "./src/views")?;
// Load from config file if it exists
if let Ok(config_path) = env::var("APP_CONFIG") {
config_builder = config_builder.add_source(File::with_name(&config_path));
} else {
// Try to load from default locations
config_builder = config_builder
.add_source(File::with_name("config/default").required(false))
.add_source(File::with_name("config/local").required(false));
}
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
config_builder =
config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
// Build and deserialize the config
let config = config_builder.build()?;
config.try_deserialize()
}
}
/// Returns the application configuration
pub fn get_config() -> AppConfig {
AppConfig::new().expect("Failed to load configuration")
}

65
src/config/oauth.rs Normal file
View File

@@ -0,0 +1,65 @@
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use serde::{Deserialize, Serialize};
use crate::config::get_app_config;
/// Gitea OAuth configuration
#[derive(Clone, Debug)]
pub struct GiteaOAuthConfig {
/// OAuth client
pub client: BasicClient,
/// Gitea instance URL
pub instance_url: String,
}
impl GiteaOAuthConfig {
/// Creates a new Gitea OAuth configuration
pub fn new() -> Self {
// Get configuration from centralized ConfigurationBuilder
let config = get_app_config();
let client_id = config.gitea_client_id()
.expect("Missing GITEA_CLIENT_ID environment variable").to_string();
let client_secret = config.gitea_client_secret()
.expect("Missing GITEA_CLIENT_SECRET environment variable").to_string();
let instance_url = config.gitea_instance_url()
.expect("Missing GITEA_INSTANCE_URL environment variable").to_string();
// Create OAuth client
let auth_url = format!("{}/login/oauth/authorize", instance_url);
let token_url = format!("{}/login/oauth/access_token", instance_url);
let client = BasicClient::new(
ClientId::new(client_id),
Some(ClientSecret::new(client_secret)),
AuthUrl::new(auth_url).unwrap(),
Some(TokenUrl::new(token_url).unwrap()),
)
.set_redirect_uri(
RedirectUrl::new(format!(
"{}/auth/gitea/callback",
config.app_url()
))
.unwrap(),
);
Self {
client,
instance_url,
}
}
}
/// Gitea user information structure
#[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser {
/// User ID
pub id: i64,
/// Username
pub login: String,
/// Full name
pub full_name: String,
/// Email address
pub email: String,
/// Avatar URL
pub avatar_url: String,
}