2024-10-12 18:21:31 -04:00
import fs from "fs" ;
import yaml from "js-yaml" ;
2025-01-01 16:40:01 -05:00
import path from "path" ;
import { z } from "zod" ;
import { fromError } from "zod-validation-error" ;
2025-01-12 20:42:16 -05:00
import {
__DIRNAME ,
APP_PATH ,
2025-02-02 15:30:41 -05:00
APP_VERSION ,
2025-01-12 20:42:16 -05:00
configFilePath1 ,
configFilePath2
} from "@server/lib/consts" ;
2025-01-03 22:32:24 -05:00
import { passwordSchema } from "@server/auth/passwordSchema" ;
2025-01-15 23:26:31 -05:00
import stoi from "./stoi" ;
2024-10-12 18:21:31 -04:00
2024-10-25 22:10:19 -04:00
const portSchema = z . number ( ) . positive ( ) . gt ( 0 ) . lte ( 65535 ) ;
2025-03-03 17:11:41 -05:00
// const hostnameSchema = z
// .string()
// .regex(
// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
// )
// .or(z.literal("localhost"));
2024-10-25 22:10:19 -04:00
2025-01-15 23:26:31 -05:00
const getEnvOrYaml = ( envVar : string ) = > ( valFromYaml : any ) = > {
return process . env [ envVar ] ? ? valFromYaml ;
} ;
const configSchema = z . object ( {
2024-10-12 18:21:31 -04:00
app : z.object ( {
2025-01-07 20:32:24 -05:00
dashboard_url : z
2024-12-30 15:48:34 -05:00
. string ( )
. url ( )
2025-01-15 23:26:31 -05:00
. optional ( )
. transform ( getEnvOrYaml ( "APP_DASHBOARDURL" ) )
. pipe ( z . string ( ) . url ( ) )
2024-12-30 15:48:34 -05:00
. transform ( ( url ) = > url . toLowerCase ( ) ) ,
2024-10-12 18:21:31 -04:00
log_level : z.enum ( [ "debug" , "info" , "warn" , "error" ] ) ,
2025-01-27 22:43:32 -05:00
save_logs : z.boolean ( ) ,
2025-01-28 21:26:34 -05:00
log_failed_attempts : z.boolean ( ) . optional ( )
2024-10-12 18:21:31 -04:00
} ) ,
2025-02-25 22:58:52 -05:00
domains : z
. record (
z . string ( ) ,
z . object ( {
2025-03-03 17:11:41 -05:00
base_domain : z
. string ( )
. nonempty ( "base_domain must not be empty" )
. transform ( ( url ) = > url . toLowerCase ( ) ) ,
2025-02-25 22:58:52 -05:00
cert_resolver : z.string ( ) . optional ( ) ,
prefer_wildcard_cert : z.boolean ( ) . optional ( )
} )
)
. refine (
( domains ) = > {
const keys = Object . keys ( domains ) ;
if ( keys . length === 0 ) {
return false ;
}
return true ;
} ,
{
message : "At least one domain must be defined"
}
)
. refine (
( domains ) = > {
const envBaseDomain = process . env . APP_BASE_DOMAIN ;
if ( envBaseDomain ) {
2025-03-03 17:11:41 -05:00
return z . string ( ) . nonempty ( ) . safeParse ( envBaseDomain ) . success ;
2025-02-25 22:58:52 -05:00
}
return true ;
} ,
{
message : "APP_BASE_DOMAIN must be a valid hostname"
}
) ,
2024-10-12 18:21:31 -04:00
server : z.object ( {
2025-01-15 23:26:31 -05:00
external_port : portSchema
. optional ( )
. transform ( getEnvOrYaml ( "SERVER_EXTERNALPORT" ) )
. transform ( stoi )
. pipe ( portSchema ) ,
internal_port : portSchema
. optional ( )
. transform ( getEnvOrYaml ( "SERVER_INTERNALPORT" ) )
. transform ( stoi )
. pipe ( portSchema ) ,
next_port : portSchema
. optional ( )
. transform ( getEnvOrYaml ( "SERVER_NEXTPORT" ) )
. transform ( stoi )
. pipe ( portSchema ) ,
2024-12-26 15:13:49 -05:00
internal_hostname : z.string ( ) . transform ( ( url ) = > url . toLowerCase ( ) ) ,
2024-11-23 23:31:22 -05:00
session_cookie_name : z.string ( ) ,
2025-01-13 23:59:10 -05:00
resource_access_token_param : z.string ( ) ,
2025-01-26 14:42:02 -05:00
resource_session_request_param : z.string ( ) ,
dashboard_session_length_hours : z
. number ( )
. positive ( )
. gt ( 0 )
. optional ( )
. default ( 720 ) ,
resource_session_length_hours : z
. number ( )
. positive ( )
. gt ( 0 )
. optional ( )
. default ( 720 ) ,
2025-01-15 23:26:31 -05:00
cors : z
. object ( {
origins : z.array ( z . string ( ) ) . optional ( ) ,
methods : z.array ( z . string ( ) ) . optional ( ) ,
allowed_headers : z.array ( z . string ( ) ) . optional ( ) ,
credentials : z.boolean ( ) . optional ( )
} )
. optional ( ) ,
trust_proxy : z.boolean ( ) . optional ( ) . default ( true )
2024-10-12 18:21:31 -04:00
} ) ,
2024-10-22 00:09:27 -04:00
traefik : z.object ( {
http_entrypoint : z.string ( ) ,
https_entrypoint : z.string ( ) . optional ( ) ,
2025-01-28 21:39:17 -05:00
additional_middlewares : z.array ( z . string ( ) ) . optional ( )
2024-10-22 00:09:27 -04:00
} ) ,
2024-10-26 16:04:01 -04:00
gerbil : z.object ( {
2025-01-15 23:26:31 -05:00
start_port : portSchema
. optional ( )
. transform ( getEnvOrYaml ( "GERBIL_STARTPORT" ) )
. transform ( stoi )
. pipe ( portSchema ) ,
base_endpoint : z
. string ( )
. optional ( )
. transform ( getEnvOrYaml ( "GERBIL_BASEENDPOINT" ) )
. pipe ( z . string ( ) )
. transform ( ( url ) = > url . toLowerCase ( ) ) ,
2024-11-24 11:05:47 -05:00
use_subdomain : z.boolean ( ) ,
2024-10-26 16:04:01 -04:00
subnet_group : z.string ( ) ,
2025-01-11 12:25:33 -05:00
block_size : z.number ( ) . positive ( ) . gt ( 0 ) ,
site_block_size : z.number ( ) . positive ( ) . gt ( 0 )
2024-10-26 16:04:01 -04:00
} ) ,
2024-12-21 21:01:12 -05:00
rate_limits : z.object ( {
global : z . object ( {
window_minutes : z.number ( ) . positive ( ) . gt ( 0 ) ,
max_requests : z.number ( ) . positive ( ) . gt ( 0 )
} ) ,
auth : z
. object ( {
window_minutes : z.number ( ) . positive ( ) . gt ( 0 ) ,
max_requests : z.number ( ) . positive ( ) . gt ( 0 )
} )
. optional ( )
2024-10-12 18:21:31 -04:00
} ) ,
email : z
. object ( {
2025-01-27 19:59:52 -05:00
smtp_host : z.string ( ) . optional ( ) ,
smtp_port : portSchema.optional ( ) ,
smtp_user : z.string ( ) . optional ( ) ,
smtp_pass : z.string ( ) . optional ( ) ,
smtp_secure : z.boolean ( ) . optional ( ) ,
2025-03-02 23:24:21 -05:00
smtp_tls_reject_unauthorized : z.boolean ( ) . optional ( ) ,
2025-01-28 21:26:34 -05:00
no_reply : z.string ( ) . email ( ) . optional ( )
2024-10-12 18:21:31 -04:00
} )
. optional ( ) ,
2024-12-25 15:54:32 -05:00
users : z.object ( {
server_admin : z.object ( {
2025-01-15 23:26:31 -05:00
email : z
. string ( )
. email ( )
. optional ( )
. transform ( getEnvOrYaml ( "USERS_SERVERADMIN_EMAIL" ) )
2025-01-21 18:36:50 -05:00
. pipe ( z . string ( ) . email ( ) )
. transform ( ( v ) = > v . toLowerCase ( ) ) ,
2025-01-03 22:32:24 -05:00
password : passwordSchema
2025-01-15 23:26:31 -05:00
. optional ( )
. transform ( getEnvOrYaml ( "USERS_SERVERADMIN_PASSWORD" ) )
. pipe ( passwordSchema )
2024-12-25 15:54:32 -05:00
} )
} ) ,
2024-10-25 00:05:43 -04:00
flags : z
. object ( {
require_email_verification : z.boolean ( ) . optional ( ) ,
2024-10-25 22:10:19 -04:00
disable_signup_without_invite : z.boolean ( ) . optional ( ) ,
2025-01-28 22:26:45 -05:00
disable_user_create_org : z.boolean ( ) . optional ( ) ,
2025-02-03 21:18:16 -05:00
allow_raw_resources : z.boolean ( ) . optional ( ) ,
2025-03-01 17:45:38 -05:00
allow_base_domain_resources : z.boolean ( ) . optional ( ) ,
allow_local_sites : z.boolean ( ) . optional ( )
2024-10-25 00:05:43 -04:00
} )
2024-12-21 21:01:12 -05:00
. optional ( )
2024-10-12 18:21:31 -04:00
} ) ;
2025-01-01 16:40:01 -05:00
export class Config {
2025-01-15 23:26:31 -05:00
private rawConfig ! : z . infer < typeof configSchema > ;
2024-12-30 15:48:34 -05:00
2025-01-01 16:40:01 -05:00
constructor ( ) {
this . loadConfig ( ) ;
2025-01-15 23:26:31 -05:00
if ( process . env . GENERATE_TRAEFIK_CONFIG === "true" ) {
this . createTraefikConfig ( ) ;
}
2025-01-01 16:40:01 -05:00
}
2024-12-30 15:48:34 -05:00
2025-01-01 16:40:01 -05:00
public loadConfig() {
const loadConfig = ( configPath : string ) = > {
2024-12-30 15:48:34 -05:00
try {
2025-01-01 16:40:01 -05:00
const yamlContent = fs . readFileSync ( configPath , "utf8" ) ;
const config = yaml . load ( yamlContent ) ;
return config ;
2024-12-30 15:48:34 -05:00
} catch ( error ) {
if ( error instanceof Error ) {
throw new Error (
2025-01-01 16:40:01 -05:00
` Error loading configuration file: ${ error . message } `
2024-12-30 15:48:34 -05:00
) ;
}
throw error ;
}
2025-01-01 16:40:01 -05:00
} ;
let environment : any ;
if ( fs . existsSync ( configFilePath1 ) ) {
environment = loadConfig ( configFilePath1 ) ;
} else if ( fs . existsSync ( configFilePath2 ) ) {
environment = loadConfig ( configFilePath2 ) ;
}
if ( ! environment ) {
const exampleConfigPath = path . join (
__DIRNAME ,
"config.example.yml"
2024-12-30 15:48:34 -05:00
) ;
2025-01-01 16:40:01 -05:00
if ( fs . existsSync ( exampleConfigPath ) ) {
try {
const exampleConfigContent = fs . readFileSync (
exampleConfigPath ,
"utf8"
) ;
fs . writeFileSync (
configFilePath1 ,
exampleConfigContent ,
"utf8"
) ;
environment = loadConfig ( configFilePath1 ) ;
} catch ( error ) {
2025-01-12 20:42:16 -05:00
console . log (
"See the docs for information about what to include in the configuration file: https://docs.fossorial.io/Pangolin/Configuration/config"
) ;
2025-01-01 16:40:01 -05:00
if ( error instanceof Error ) {
throw new Error (
` Error creating configuration file from example: ${
error . message
} `
) ;
}
throw error ;
}
} else {
throw new Error (
"No configuration file found and no example configuration available"
) ;
}
2024-12-30 15:48:34 -05:00
}
2024-10-12 18:21:31 -04:00
2025-01-01 16:40:01 -05:00
if ( ! environment ) {
throw new Error ( "No configuration file found" ) ;
}
2024-10-12 18:21:31 -04:00
2025-01-15 23:26:31 -05:00
const parsedConfig = configSchema . safeParse ( environment ) ;
2024-10-12 18:21:31 -04:00
2025-01-01 16:40:01 -05:00
if ( ! parsedConfig . success ) {
const errors = fromError ( parsedConfig . error ) ;
throw new Error ( ` Invalid configuration file: ${ errors } ` ) ;
}
2024-10-12 18:21:31 -04:00
2025-02-02 15:30:41 -05:00
process . env . APP_VERSION = APP_VERSION ;
2024-12-24 12:06:13 -05:00
2025-01-01 16:40:01 -05:00
process . env . NEXT_PORT = parsedConfig . data . server . next_port . toString ( ) ;
process . env . SERVER_EXTERNAL_PORT =
parsedConfig . data . server . external_port . toString ( ) ;
process . env . SERVER_INTERNAL_PORT =
parsedConfig . data . server . internal_port . toString ( ) ;
process . env . FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig . data . flags
? . require_email_verification
? "true"
: "false" ;
2025-01-28 22:26:45 -05:00
process . env . FLAGS_ALLOW_RAW_RESOURCES = parsedConfig . data . flags
2025-02-03 21:18:16 -05:00
? . allow_raw_resources
? "true"
: "false" ;
2025-01-01 16:40:01 -05:00
process . env . SESSION_COOKIE_NAME =
parsedConfig . data . server . session_cookie_name ;
process . env . EMAIL_ENABLED = parsedConfig . data . email ? "true" : "false" ;
process . env . DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig . data . flags
? . disable_signup_without_invite
? "true"
: "false" ;
process . env . DISABLE_USER_CREATE_ORG = parsedConfig . data . flags
? . disable_user_create_org
? "true"
: "false" ;
2025-01-12 20:42:16 -05:00
process . env . RESOURCE_ACCESS_TOKEN_PARAM =
parsedConfig . data . server . resource_access_token_param ;
2025-01-26 14:42:02 -05:00
process . env . RESOURCE_SESSION_REQUEST_PARAM =
parsedConfig . data . server . resource_session_request_param ;
2025-02-03 21:18:16 -05:00
process . env . FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig . data . flags
? . allow_base_domain_resources
? "true"
: "false" ;
2025-02-04 22:14:11 -05:00
process . env . DASHBOARD_URL = parsedConfig . data . app . dashboard_url ;
2025-01-01 16:40:01 -05:00
2025-02-25 22:58:52 -05:00
if ( process . env . APP_BASE_DOMAIN ) {
console . log (
` DEPRECATED! APP_BASE_DOMAIN is deprecated and will be removed in a future release. Use the domains section in the configuration file instead. See https://docs.fossorial.io/Pangolin/Configuration/config for more information. `
) ;
parsedConfig . data . domains . domain1 = {
base_domain : process.env.APP_BASE_DOMAIN ,
cert_resolver : "letsencrypt"
} ;
}
2025-01-01 16:40:01 -05:00
this . rawConfig = parsedConfig . data ;
}
2025-01-01 17:50:12 -05:00
public getRawConfig() {
return this . rawConfig ;
}
2025-01-28 21:26:34 -05:00
public getNoReplyEmail ( ) : string | undefined {
return (
this . rawConfig . email ? . no_reply || this . rawConfig . email ? . smtp_user
) ;
}
2025-02-23 23:03:40 -05:00
public getDomain ( domainId : string ) {
return this . rawConfig . domains [ domainId ] ;
}
2025-01-15 23:26:31 -05:00
private createTraefikConfig() {
try {
// check if traefik_config.yml and dynamic_config.yml exists in APP_PATH/traefik
const defaultTraefikConfigPath = path . join (
__DIRNAME ,
"traefik_config.example.yml"
) ;
const defaultDynamicConfigPath = path . join (
__DIRNAME ,
"dynamic_config.example.yml"
) ;
const traefikPath = path . join ( APP_PATH , "traefik" ) ;
if ( ! fs . existsSync ( traefikPath ) ) {
return ;
}
// load default configs
let traefikConfig = fs . readFileSync (
defaultTraefikConfigPath ,
"utf8"
) ;
let dynamicConfig = fs . readFileSync (
defaultDynamicConfigPath ,
"utf8"
) ;
traefikConfig = traefikConfig
. split ( "{{.LetsEncryptEmail}}" )
. join ( this . rawConfig . users . server_admin . email ) ;
traefikConfig = traefikConfig
. split ( "{{.INTERNAL_PORT}}" )
. join ( this . rawConfig . server . internal_port . toString ( ) ) ;
dynamicConfig = dynamicConfig
. split ( "{{.DashboardDomain}}" )
. join ( new URL ( this . rawConfig . app . dashboard_url ) . hostname ) ;
dynamicConfig = dynamicConfig
. split ( "{{.NEXT_PORT}}" )
. join ( this . rawConfig . server . next_port . toString ( ) ) ;
dynamicConfig = dynamicConfig
. split ( "{{.EXTERNAL_PORT}}" )
. join ( this . rawConfig . server . external_port . toString ( ) ) ;
// write thiese to the traefik directory
const traefikConfigPath = path . join (
traefikPath ,
"traefik_config.yml"
) ;
const dynamicConfigPath = path . join (
traefikPath ,
"dynamic_config.yml"
) ;
fs . writeFileSync ( traefikConfigPath , traefikConfig , "utf8" ) ;
fs . writeFileSync ( dynamicConfigPath , dynamicConfig , "utf8" ) ;
console . log ( "Traefik configuration files created" ) ;
} catch ( e ) {
console . log (
"Failed to generate the Traefik configuration files. Please create them manually."
) ;
console . error ( e ) ;
}
}
2024-12-30 15:48:34 -05:00
}
2024-10-12 21:23:12 -04:00
2025-01-01 16:40:01 -05:00
export const config = new Config ( ) ;
export default config ;