import fs from "fs"; import yaml from "js-yaml"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { __DIRNAME, APP_VERSION, configFilePath1, configFilePath2 } from "@server/lib/consts"; import { passwordSchema } from "@server/auth/passwordSchema"; import stoi from "./stoi"; const portSchema = z.number().positive().gt(0).lte(65535); const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { return process.env[envVar] ?? valFromYaml; }; const configSchema = z.object({ app: z.object({ dashboard_url: z .string() .url() .optional() .pipe(z.string().url()) .transform((url) => url.toLowerCase()), log_level: z.enum(["debug", "info", "warn", "error"]), save_logs: z.boolean(), log_failed_attempts: z.boolean().optional() }), domains: z .record( z.string(), z.object({ base_domain: z .string() .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), 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" } ), server: z.object({ external_port: portSchema.optional().transform(stoi).pipe(portSchema), internal_port: portSchema.optional().transform(stoi).pipe(portSchema), next_port: portSchema.optional().transform(stoi).pipe(portSchema), internal_hostname: z.string().transform((url) => url.toLowerCase()), session_cookie_name: z.string(), resource_access_token_param: z.string(), 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), 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) }), traefik: z.object({ http_entrypoint: z.string(), https_entrypoint: z.string().optional(), additional_middlewares: z.array(z.string()).optional() }), gerbil: z.object({ start_port: portSchema.optional().transform(stoi).pipe(portSchema), base_endpoint: z .string() .optional() .pipe(z.string()) .transform((url) => url.toLowerCase()), use_subdomain: z.boolean(), subnet_group: z.string(), block_size: z.number().positive().gt(0), site_block_size: z.number().positive().gt(0) }), 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() }), email: z .object({ smtp_host: z.string().optional(), smtp_port: portSchema.optional(), smtp_user: z.string().optional(), smtp_pass: z.string().optional(), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() }) .optional(), users: z.object({ server_admin: z.object({ email: z .string() .email() .optional() .transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL")) .pipe(z.string().email()) .transform((v) => v.toLowerCase()), password: passwordSchema .optional() .transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD")) .pipe(passwordSchema) }) }), flags: z .object({ require_email_verification: z.boolean().optional(), disable_signup_without_invite: z.boolean().optional(), disable_user_create_org: z.boolean().optional(), allow_raw_resources: z.boolean().optional(), allow_base_domain_resources: z.boolean().optional(), allow_local_sites: z.boolean().optional() }) .optional() }); export class Config { private rawConfig!: z.infer; constructor() { this.loadConfig(); } public loadConfig() { const loadConfig = (configPath: string) => { try { const yamlContent = fs.readFileSync(configPath, "utf8"); const config = yaml.load(yamlContent); return config; } catch (error) { if (error instanceof Error) { throw new Error( `Error loading configuration file: ${error.message}` ); } throw error; } }; let environment: any; if (fs.existsSync(configFilePath1)) { environment = loadConfig(configFilePath1); } else if (fs.existsSync(configFilePath2)) { environment = loadConfig(configFilePath2); } if (process.env.APP_BASE_DOMAIN) { console.log("You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"); } if (!environment) { throw new Error( "No configuration file found. Please create one. https://docs.fossorial.io/" ); } const parsedConfig = configSchema.safeParse(environment); if (!parsedConfig.success) { const errors = fromError(parsedConfig.error); throw new Error(`Invalid configuration file: ${errors}`); } process.env.APP_VERSION = APP_VERSION; 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"; process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags ?.allow_raw_resources ? "true" : "false"; 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"; process.env.RESOURCE_ACCESS_TOKEN_PARAM = parsedConfig.data.server.resource_access_token_param; process.env.RESOURCE_SESSION_REQUEST_PARAM = parsedConfig.data.server.resource_session_request_param; process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags ?.allow_base_domain_resources ? "true" : "false"; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; this.rawConfig = parsedConfig.data; } public getRawConfig() { return this.rawConfig; } public getNoReplyEmail(): string | undefined { return ( this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user ); } public getDomain(domainId: string) { return this.rawConfig.domains[domainId]; } } export const config = new Config(); export default config;