use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default)]
pub server: ServerSection,
#[serde(default)]
pub storage: StorageBackendConfig,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub notifications: NotificationsConfig,
#[serde(default)]
pub security: SecurityConfig,
#[serde(default)]
pub extras: ExtrasConfig,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ExtrasConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub conneg_enabled: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cors_allowed_origins: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_body_size_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_acl_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit_writes_per_min: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subdomains_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idp_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invite_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerSection {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
}
impl Default for ServerSection {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
base_url: None,
}
}
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
3000
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum StorageBackendConfig {
Fs {
#[serde(default = "default_fs_root")]
root: String,
},
Memory,
S3 {
bucket: String,
region: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prefix: Option<String>,
},
}
impl Default for StorageBackendConfig {
fn default() -> Self {
Self::Fs {
root: default_fs_root(),
}
}
}
fn default_fs_root() -> String {
"./data".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
#[serde(default = "default_true")]
pub nip98_enabled: bool,
#[serde(default)]
pub oidc_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oidc_issuer: Option<String>,
#[serde(default = "default_dpop_ttl")]
pub dpop_replay_ttl_seconds: u64,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
nip98_enabled: true,
oidc_enabled: false,
oidc_issuer: None,
dpop_replay_ttl_seconds: default_dpop_ttl(),
}
}
}
fn default_true() -> bool {
true
}
fn default_dpop_ttl() -> u64 {
300
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationsConfig {
#[serde(default = "default_true")]
pub ws2023_enabled: bool,
#[serde(default)]
pub webhook2023_enabled: bool,
#[serde(default = "default_true")]
pub legacy_solid_01_enabled: bool,
}
impl Default for NotificationsConfig {
fn default() -> Self {
Self {
ws2023_enabled: true,
webhook2023_enabled: false,
legacy_solid_01_enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
#[serde(default)]
pub ssrf_allow_private: bool,
#[serde(default)]
pub ssrf_allowlist: Vec<String>,
#[serde(default)]
pub ssrf_denylist: Vec<String>,
#[serde(default = "default_dotfile_allowlist")]
pub dotfile_allowlist: Vec<String>,
#[serde(default = "default_true")]
pub acl_origin_enabled: bool,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
ssrf_allow_private: false,
ssrf_allowlist: Vec::new(),
ssrf_denylist: Vec::new(),
dotfile_allowlist: default_dotfile_allowlist(),
acl_origin_enabled: true,
}
}
}
fn default_dotfile_allowlist() -> Vec<String> {
vec![
".acl".to_string(),
".meta".to_string(),
".account".to_string(),
]
}
impl ServerConfig {
pub fn validate(&self) -> Result<(), String> {
if self.auth.oidc_enabled && self.auth.oidc_issuer.is_none() {
return Err(
"auth.oidc_enabled=true but auth.oidc_issuer is not set (set JSS_OIDC_ISSUER)"
.to_string(),
);
}
if let StorageBackendConfig::S3 { bucket, region, .. } = &self.storage {
if bucket.is_empty() {
return Err("storage.type=s3 but storage.bucket is empty".to_string());
}
if region.is_empty() {
return Err("storage.type=s3 but storage.region is empty".to_string());
}
}
Ok(())
}
}