skynet_api 0.6.5

API for Skynet plugin.
Documentation
use std::path::Path;

use actix_cloud::config;
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
use validator::{Validate, ValidationError};

pub const CONFIG_SESSION_KEY: &str = "config.session.key";
pub const CONFIG_SESSION_EXPIRE: &str = "config.session.expire";
pub const CONFIG_SESSION_REMEMBER: &str = "config.session.remember";
pub const CONFIG_WEBPUSH_TTL: &str = "config.webpush.ttl";
pub const CONFIG_WEBPUSH_KEY: &str = "config.webpush.key";
pub const CONFIG_WEBPUSH_ENDPOINT: &str = "config.webpush.endpoint";

fn check_file(path: &str) -> Result<(), ValidationError> {
    if Path::new(path).exists() {
        Ok(())
    } else {
        Err(ValidationError::new("file not exist"))
    }
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigListen {
    #[serde_inline_default("0.0.0.0:8080".into())]
    #[validate(length(min = 1))]
    pub address: String,
    #[serde(default)]
    pub worker: usize,
    #[serde(default)]
    pub ssl: bool,
    #[validate(length(min = 1))]
    pub ssl_cert: Option<String>,
    #[validate(length(min = 1))]
    pub ssl_key: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigDatabase {
    #[validate(length(min = 1))]
    pub dsn: String,
}

#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigRedis {
    #[serde(default)]
    pub enable: bool,
    #[validate(length(min = 1))]
    pub dsn: Option<String>,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigSession {
    #[serde_inline_default("session_".into())]
    #[validate(length(min = 1))]
    pub prefix: String,
    #[serde_inline_default("SESSIONID".into())]
    #[validate(length(min = 1))]
    pub cookie: String,
    #[serde(default)]
    pub refresh: bool,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigHeader {
    #[serde_inline_default("default-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'self'".into())]
    #[validate(length(min = 1))]
    pub csp: String,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigProxy {
    #[serde(default)]
    pub enable: bool,
    #[serde_inline_default("X-Real-Address".into())]
    #[validate(length(min = 1))]
    pub header: String,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigCsrf {
    #[serde_inline_default("csrf_".into())]
    #[validate(length(min = 1))]
    pub prefix: String,
    #[serde_inline_default(10)]
    #[validate(range(min = 1))]
    pub expire: u32,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigRecaptcha {
    #[serde(default)]
    pub enable: bool,
    #[serde_inline_default("https://www.recaptcha.net".into())]
    #[validate(length(min = 1))]
    pub url: String,
    #[validate(length(min = 1))]
    pub sitekey: Option<String>,
    #[validate(length(min = 1))]
    pub secret: Option<String>,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigClient {
    #[validate(length(min = 1))]
    pub proxy: Option<String>,
    #[validate(length(min = 1))]
    pub username: Option<String>,
    #[validate(length(min = 1))]
    pub password: Option<String>,
    #[serde_inline_default(10)]
    pub timeout: u32,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct ConfigGeoip {
    #[serde(default)]
    pub enable: bool,
    #[serde_inline_default("GeoLite2-City.mmdb".into())]
    pub database: String,
}

#[serde_inline_default]
#[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct Config {
    #[validate(nested)]
    pub database: ConfigDatabase,
    #[validate(nested)]
    pub redis: ConfigRedis,
    #[validate(nested)]
    pub session: ConfigSession,
    #[validate(nested)]
    pub listen: ConfigListen,
    #[validate(nested)]
    pub header: ConfigHeader,
    #[validate(nested)]
    pub proxy: ConfigProxy,
    #[validate(nested)]
    pub recaptcha: ConfigRecaptcha,
    #[validate(nested)]
    pub csrf: ConfigCsrf,
    #[validate(nested)]
    pub geoip: ConfigGeoip,
    #[validate(nested)]
    pub client: ConfigClient,
    #[serde_inline_default("default.webp".into())]
    #[validate(custom(function = "check_file"))]
    pub avatar: String,
    #[serde_inline_default("en-US".into())]
    #[validate(length(min = 1))]
    pub lang: String,
}

pub fn load_file<S: AsRef<str>>(path: S) -> (config::Config, Config) {
    let cfg = config::Config::builder()
        .add_source(config::File::with_name(path.as_ref()))
        .build()
        .expect("Config load failed");
    let ret: Config = cfg.clone().try_deserialize().expect("Config file invalid");
    let _ = ret.validate().map_err(|e| panic!("{}", e.to_string()));
    // additional checker
    if ret.listen.ssl {
        assert!(
            ret.listen.ssl_key.is_some(),
            "`listen.ssl_key` is not provided"
        );
        assert!(
            ret.listen.ssl_cert.is_some(),
            "`listen.ssl_cert` is not provided"
        );
    }
    if ret.recaptcha.enable {
        assert!(
            ret.recaptcha.sitekey.is_some(),
            "`recaptcha.sitekey` is not provided"
        );
        assert!(
            ret.recaptcha.secret.is_some(),
            "`recaptcha.secret` is not provided"
        );
    }
    if ret.redis.enable {
        assert!(ret.redis.dsn.is_some(), "`redis.dsn` is not provided");
    }
    if ret.geoip.enable {
        assert!(
            check_file(&ret.geoip.database).is_ok(),
            "`geoip.database` file not exist"
        );
    }
    (cfg, ret)
}