use crate::settings::{OAuth2Configuration, Security, Settings};
use crate::{cmd::root::Cli, data::bigquery};
use crate::{data, security};
use actix_web::dev::ServiceRequest;
use actix_web::http::header;
use actix_web::web::ServiceConfig;
use clap::Parser;
use colored::Colorize;
use env_logger::{Builder, Env};
use jsonwebtoken::jwk::JwkSet;
use log::{info, warn};
use reqwest_middleware::ClientBuilder;
use sea_orm::DatabaseConnection;
use std::any::Any;
use std::io::Write;
use std::sync::OnceLock;
use thiserror::Error;
use tracing::{debug, error};
#[cfg(feature = "memory-database")]
use sea_orm::MockDatabase;
#[cfg(feature = "memory-database")]
use std::sync::Arc;
static SERVER: OnceLock<Box<dyn GlobalServer + Send + Sync>> = OnceLock::new();
#[derive(Clone)]
pub struct Server {
running: bool,
args: Option<Cli>,
settings: Option<Settings>,
fnconfig: Option<fn(&mut ServiceConfig)>,
database: Option<data::ServerDatabase>,
}
impl Server {
pub fn global() -> Result<&'static (dyn GlobalServer + Send + Sync)> {
SERVER
.get()
.map(|server| server.as_ref())
.ok_or(ServerError::NotInitialized)
}
pub fn global_server() -> Option<&'static Server> {
SERVER
.get()
.map(|s| s.as_any().downcast_ref::<Server>())
.unwrap_or_default()
}
pub fn set_global(server: Server) {
if SERVER.set(Box::new(server)).is_err() {
debug!(
"{}",
"Server is already initialized. The new instance will be ignored.".yellow()
);
}
}
fn check_initialized() {
if SERVER.get().is_some() {
error!("{}", "Server is already initialized.".red());
panic!()
}
}
fn preflight(app_version: String, banner: Option<String>) {
Server::configure_log().expect("Log is already initialized!");
let _standard_ascii_art = r#"
____ _ ____
| _ \ _ _ ___ | |_ / ___| ___ _ __ __ __ ___ _ __
| |_) || | | |/ __|| __| \___ \ / _ \| '__|\ \ / // _ \| '__|
| _ < | |_| |\__ \| |_ ___) || __/| | \ V /| __/| |
|_| \_\ \__,_||___/ \__| |____/ \___||_| \_/ \___||_|
"#;
let ascii_art = r#"
___ __ ____
/ _ \ __ __ ___ / /_ / __/___ ____ _ __ ___ ____
/ , _// // /(_-</ __/ _\ \ / -_)/ __/| |/ // -_)/ __/
/_/|_| \_,_//___/\__/ /___/ \__//_/ |___/ \__//_/
"#;
if let Some(banner) = banner
&& !banner.is_empty()
{
println!("{}", banner);
} else {
println!("{}", ascii_art);
}
println!(
"\t{} {}\n\t{} {}\n\t{} {}\n",
"License:".green(),
env!("CARGO_PKG_LICENSE").bright_blue(),
"Server Version:".green(),
env!("CARGO_PKG_VERSION").bright_blue(),
"Application Version:".green(),
app_version.bright_blue(),
);
}
pub fn new(app_version: String, banner: Option<String>) -> Self {
Server::check_initialized();
Server::preflight(app_version, banner);
Server {
running: false,
args: None,
settings: None,
fnconfig: None,
database: None,
}
}
#[cfg(feature = "memory-database")]
pub fn new_with_mock_database(name: String, database: MockDatabase) -> Self {
Server::preflight("".into(), None);
let databases = data::ServerDatabase::new_with_mock_database(name, database);
Server {
running: false,
args: None,
settings: None,
fnconfig: None,
database: Some(databases),
}
}
#[cfg(feature = "memory-database")]
pub async fn new_with_memory_database(name: String) -> Result<Self> {
Server::preflight("".into(), None);
let databases = data::ServerDatabase::new_with_memory_database(name)
.await
.map_err(|e| ServerError::Database(e.to_string()))?;
Ok(Server {
running: false,
args: None,
settings: None,
fnconfig: None,
database: Some(databases),
})
}
pub async fn new_with_settings(settings: Settings) -> Result<Self> {
Server::preflight("".into(), None);
let result = Server::discover_oauth_security_settings(&settings).await;
let settings = match result {
Ok(s) => s,
Err(e) => {
info!("Failed to discover OAuth2 security settings: {}", e);
settings
}
};
let server = Server {
running: false,
args: None,
settings: Some(settings),
fnconfig: None,
database: None,
};
Ok(server)
}
pub async fn init(mut self) -> Result<Self> {
Server::check_initialized();
let args = Cli::parse();
let settings =
Cli::load_config(&args).map_err(|e| ServerError::Configuration(e.to_string()))?;
let result = Server::discover_oauth_security_settings(&settings).await;
let settings = match result {
Ok(s) => s,
Err(e) => {
info!("Failed to discover OAuth2 security settings: {}", e);
settings
}
};
self.settings = Some(settings);
self.args = Some(args);
Ok(self)
}
async fn discover_oauth_security_settings(settings: &Settings) -> Result<Settings> {
let oauth2 = settings
.security
.as_ref()
.and_then(|s| s.oauth2.as_ref())
.ok_or(ServerError::Configuration(
"Oauth2 security settings not found.".to_string(),
))?;
if oauth2.discovery_enabled.unwrap_or(false)
&& let Some(discovery_url) = &oauth2.discovery_url
{
let client = ClientBuilder::new(reqwest::Client::new()).build();
info!(
"Discovering OAuth2 security settings from {}",
discovery_url.bright_blue()
);
let mut discovery = client
.get(discovery_url)
.send()
.await
.map_err(|e| {
info!("Failed to fetch OAuth2 discovery settings: {}", e);
ServerError::Configuration(e.to_string())
})?
.json::<OAuth2Configuration>()
.await
.map_err(|e| {
info!("Failed to parse OAuth2 discovery settings: {}", e);
ServerError::Configuration(e.to_string())
})?;
discovery.enabled = oauth2.enabled;
discovery.discovery_url = Some(discovery_url.clone());
discovery.discovery_enabled = Some(true);
if let Some(jwks_uri) = discovery.jwks_uri.clone() {
info!("Fetching JWKs Certs from {}", jwks_uri.bright_blue());
let jwks = client
.get(jwks_uri)
.send()
.await
.map_err(|e| {
info!("Failed to fetch JWKs: {}", e);
ServerError::Configuration(e.to_string())
})?
.json::<JwkSet>()
.await
.map_err(|e| {
info!("Failed to parse JWKs: {}", e);
ServerError::Configuration(e.to_string())
})?;
discovery.jwks = Some(jwks);
}
let mut settings = settings.clone();
settings.security = Some(Security {
oauth2: Some(discovery),
});
return Ok(settings);
}
Ok(settings.clone())
}
fn configure_log() -> Result<()> {
let level = Env::default().default_filter_or("info,actix_web=error,actix_web_prom=error");
let _ = Builder::from_env(level)
.format(|buf, record| {
let level = match record.level() {
log::Level::Info => record.level().as_str().bright_green(),
log::Level::Debug => record.level().as_str().bright_blue(),
log::Level::Trace => record.level().as_str().bright_cyan(),
log::Level::Warn => record.level().as_str().bright_yellow(),
log::Level::Error => record.level().as_str().bright_red(),
};
let datetime = chrono::Local::now()
.format("%d-%m-%YT%H:%M:%S%.3f%:z")
.to_string()
.white();
writeln!(
buf,
"{:<24} {:<5} [{:<60}] - {}",
datetime, level, record.module_path().unwrap_or("unknown").blue(), record.args() )
})
.try_init();
Ok(())
}
pub fn configure(mut self, fnconfig: Option<fn(&mut ServiceConfig)>) -> Self {
Server::check_initialized();
self.fnconfig = fnconfig;
self
}
pub async fn intialize_database(mut self) -> Result<Self> {
Server::check_initialized();
let settings = self.settings.as_ref().ok_or_else(|| {
ServerError::InvalidState("Cannot initialize database before calling init()".into())
})?;
let database = data::ServerDatabase::new_with_settings(settings)
.await
.map_err(|e| ServerError::Database(e.to_string()))?;
self.database = Some(database);
Ok(self)
}
pub async fn run(&self) {
if self.running {
warn!("The server is already running and cannot be started again.");
return;
}
self.clone().running = true;
if let (Some(args), Some(settings)) = (&self.args, &self.settings) {
Cli::init(args, settings, self.fnconfig).await;
}
if let Some(database) = &self.database {
database.close();
}
}
}
impl Default for Server {
fn default() -> Self {
Server::new("".into(), None)
}
}
pub trait GlobalServer {
fn as_any(&self) -> &dyn Any;
fn settings(&self) -> &Settings;
#[cfg(feature = "memory-database")]
fn database_with_name(&self, name: &str) -> Result<Arc<DatabaseConnection>>;
#[cfg(not(feature = "memory-database"))]
fn database_with_name(&self, name: &str) -> Result<DatabaseConnection>;
fn bigquery(&self) -> Option<&bigquery::BigQueryClient>;
fn is_running(&self) -> bool;
fn validate_jwt(
&self,
request: &ServiceRequest,
authorize: String,
) -> security::oauth2::Result<()>;
}
impl GlobalServer for Server {
fn as_any(&self) -> &dyn Any {
self
}
#[cfg(feature = "memory-database")]
fn database_with_name(&self, name: &str) -> Result<Arc<DatabaseConnection>> {
let database = self
.database
.clone()
.ok_or_else(|| ServerError::InvalidState("Database not initialized".into()))?;
for database in database.databases {
if database.name == name {
return Ok(database.connection);
}
}
Err(ServerError::Database("Database not found".into()))
}
#[cfg(not(feature = "memory-database"))]
fn database_with_name(&self, name: &str) -> Result<DatabaseConnection> {
let database = self
.database
.as_ref()
.ok_or_else(|| ServerError::InvalidState("Database not initialized".into()))?;
for database in &database.databases {
if database.name == name {
return Ok(database.connection.clone());
}
}
Err(ServerError::Database("Database not found".into()))
}
fn settings(&self) -> &Settings {
self.settings
.as_ref()
.expect("Settings must be initialized before calling settings()")
}
fn bigquery(&self) -> Option<&bigquery::BigQueryClient> {
self.database.as_ref().and_then(|db| db.bigquery.as_ref())
}
fn is_running(&self) -> bool {
self.running
}
fn validate_jwt(
&self,
request: &ServiceRequest,
authorize: String,
) -> security::oauth2::Result<()> {
let token: &str = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
security::oauth2::OAuth2Error::InvalidJwt("Invalid JWT Header in request.".into())
})?
.trim_start_matches("Bearer ");
let settings = self.settings.as_ref().ok_or_else(|| {
warn!("Settings not configured.");
security::oauth2::OAuth2Error::Configuration("Settings not configured.".into())
})?;
security::oauth2::validate_jwt(token, settings, authorize)?;
Ok(())
}
}
pub type Result<T, E = ServerError> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum ServerError {
#[error("Invalid server state: {0}")]
InvalidState(String),
#[error("Invalid server configuration: {0}")]
Configuration(String),
#[error("Tokio runtime not found. Details: {0}")]
RuntimeNotFound(String),
#[error("Server database error: {0}")]
Database(String),
#[error("Server is not initialized.")]
NotInitialized,
#[error("Server is already initialized. The new instance will be ignored.")]
AlreadyInitialized,
}