config.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import fs from "fs";
  2. import yaml from "js-yaml";
  3. import path from "path";
  4. import { z } from "zod";
  5. import { fromError } from "zod-validation-error";
  6. import {
  7. __DIRNAME,
  8. APP_PATH,
  9. APP_VERSION,
  10. configFilePath1,
  11. configFilePath2
  12. } from "@server/lib/consts";
  13. import { passwordSchema } from "@server/auth/passwordSchema";
  14. import stoi from "./stoi";
  15. const portSchema = z.number().positive().gt(0).lte(65535);
  16. const hostnameSchema = z
  17. .string()
  18. .regex(
  19. /^(?:(?: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])$/
  20. )
  21. .or(z.literal("localhost"));
  22. const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
  23. return process.env[envVar] ?? valFromYaml;
  24. };
  25. const configSchema = z.object({
  26. app: z.object({
  27. dashboard_url: z
  28. .string()
  29. .url()
  30. .optional()
  31. .transform(getEnvOrYaml("APP_DASHBOARDURL"))
  32. .pipe(z.string().url())
  33. .transform((url) => url.toLowerCase()),
  34. base_domain: hostnameSchema
  35. .optional()
  36. .transform(getEnvOrYaml("APP_BASEDOMAIN"))
  37. .pipe(hostnameSchema)
  38. .transform((url) => url.toLowerCase()),
  39. log_level: z.enum(["debug", "info", "warn", "error"]),
  40. save_logs: z.boolean(),
  41. log_failed_attempts: z.boolean().optional()
  42. }),
  43. server: z.object({
  44. external_port: portSchema
  45. .optional()
  46. .transform(getEnvOrYaml("SERVER_EXTERNALPORT"))
  47. .transform(stoi)
  48. .pipe(portSchema),
  49. internal_port: portSchema
  50. .optional()
  51. .transform(getEnvOrYaml("SERVER_INTERNALPORT"))
  52. .transform(stoi)
  53. .pipe(portSchema),
  54. next_port: portSchema
  55. .optional()
  56. .transform(getEnvOrYaml("SERVER_NEXTPORT"))
  57. .transform(stoi)
  58. .pipe(portSchema),
  59. internal_hostname: z.string().transform((url) => url.toLowerCase()),
  60. session_cookie_name: z.string(),
  61. resource_access_token_param: z.string(),
  62. resource_session_request_param: z.string(),
  63. dashboard_session_length_hours: z
  64. .number()
  65. .positive()
  66. .gt(0)
  67. .optional()
  68. .default(720),
  69. resource_session_length_hours: z
  70. .number()
  71. .positive()
  72. .gt(0)
  73. .optional()
  74. .default(720),
  75. cors: z
  76. .object({
  77. origins: z.array(z.string()).optional(),
  78. methods: z.array(z.string()).optional(),
  79. allowed_headers: z.array(z.string()).optional(),
  80. credentials: z.boolean().optional()
  81. })
  82. .optional(),
  83. trust_proxy: z.boolean().optional().default(true)
  84. }),
  85. traefik: z.object({
  86. http_entrypoint: z.string(),
  87. https_entrypoint: z.string().optional(),
  88. cert_resolver: z.string().optional(),
  89. prefer_wildcard_cert: z.boolean().optional(),
  90. additional_middlewares: z.array(z.string()).optional()
  91. }),
  92. gerbil: z.object({
  93. start_port: portSchema
  94. .optional()
  95. .transform(getEnvOrYaml("GERBIL_STARTPORT"))
  96. .transform(stoi)
  97. .pipe(portSchema),
  98. base_endpoint: z
  99. .string()
  100. .optional()
  101. .transform(getEnvOrYaml("GERBIL_BASEENDPOINT"))
  102. .pipe(z.string())
  103. .transform((url) => url.toLowerCase()),
  104. use_subdomain: z.boolean(),
  105. subnet_group: z.string(),
  106. block_size: z.number().positive().gt(0),
  107. site_block_size: z.number().positive().gt(0)
  108. }),
  109. rate_limits: z.object({
  110. global: z.object({
  111. window_minutes: z.number().positive().gt(0),
  112. max_requests: z.number().positive().gt(0)
  113. }),
  114. auth: z
  115. .object({
  116. window_minutes: z.number().positive().gt(0),
  117. max_requests: z.number().positive().gt(0)
  118. })
  119. .optional()
  120. }),
  121. email: z
  122. .object({
  123. smtp_host: z.string().optional(),
  124. smtp_port: portSchema.optional(),
  125. smtp_user: z.string().optional(),
  126. smtp_pass: z.string().optional(),
  127. smtp_secure: z.boolean().optional(),
  128. no_reply: z.string().email().optional()
  129. })
  130. .optional(),
  131. users: z.object({
  132. server_admin: z.object({
  133. email: z
  134. .string()
  135. .email()
  136. .optional()
  137. .transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
  138. .pipe(z.string().email())
  139. .transform((v) => v.toLowerCase()),
  140. password: passwordSchema
  141. .optional()
  142. .transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
  143. .pipe(passwordSchema)
  144. })
  145. }),
  146. flags: z
  147. .object({
  148. require_email_verification: z.boolean().optional(),
  149. disable_signup_without_invite: z.boolean().optional(),
  150. disable_user_create_org: z.boolean().optional(),
  151. allow_raw_resources: z.boolean().optional(),
  152. allow_base_domain_resources: z.boolean().optional()
  153. })
  154. .optional()
  155. });
  156. export class Config {
  157. private rawConfig!: z.infer<typeof configSchema>;
  158. constructor() {
  159. this.loadConfig();
  160. if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
  161. this.createTraefikConfig();
  162. }
  163. }
  164. public loadEnvironment() {}
  165. public loadConfig() {
  166. const loadConfig = (configPath: string) => {
  167. try {
  168. const yamlContent = fs.readFileSync(configPath, "utf8");
  169. const config = yaml.load(yamlContent);
  170. return config;
  171. } catch (error) {
  172. if (error instanceof Error) {
  173. throw new Error(
  174. `Error loading configuration file: ${error.message}`
  175. );
  176. }
  177. throw error;
  178. }
  179. };
  180. let environment: any;
  181. if (fs.existsSync(configFilePath1)) {
  182. environment = loadConfig(configFilePath1);
  183. } else if (fs.existsSync(configFilePath2)) {
  184. environment = loadConfig(configFilePath2);
  185. }
  186. if (!environment) {
  187. const exampleConfigPath = path.join(
  188. __DIRNAME,
  189. "config.example.yml"
  190. );
  191. if (fs.existsSync(exampleConfigPath)) {
  192. try {
  193. const exampleConfigContent = fs.readFileSync(
  194. exampleConfigPath,
  195. "utf8"
  196. );
  197. fs.writeFileSync(
  198. configFilePath1,
  199. exampleConfigContent,
  200. "utf8"
  201. );
  202. environment = loadConfig(configFilePath1);
  203. } catch (error) {
  204. console.log(
  205. "See the docs for information about what to include in the configuration file: https://docs.fossorial.io/Pangolin/Configuration/config"
  206. );
  207. if (error instanceof Error) {
  208. throw new Error(
  209. `Error creating configuration file from example: ${
  210. error.message
  211. }`
  212. );
  213. }
  214. throw error;
  215. }
  216. } else {
  217. throw new Error(
  218. "No configuration file found and no example configuration available"
  219. );
  220. }
  221. }
  222. if (!environment) {
  223. throw new Error("No configuration file found");
  224. }
  225. const parsedConfig = configSchema.safeParse(environment);
  226. if (!parsedConfig.success) {
  227. const errors = fromError(parsedConfig.error);
  228. throw new Error(`Invalid configuration file: ${errors}`);
  229. }
  230. process.env.APP_VERSION = APP_VERSION;
  231. process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
  232. process.env.SERVER_EXTERNAL_PORT =
  233. parsedConfig.data.server.external_port.toString();
  234. process.env.SERVER_INTERNAL_PORT =
  235. parsedConfig.data.server.internal_port.toString();
  236. process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
  237. ?.require_email_verification
  238. ? "true"
  239. : "false";
  240. process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
  241. ?.allow_raw_resources
  242. ? "true"
  243. : "false";
  244. process.env.SESSION_COOKIE_NAME =
  245. parsedConfig.data.server.session_cookie_name;
  246. process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
  247. process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
  248. ?.disable_signup_without_invite
  249. ? "true"
  250. : "false";
  251. process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
  252. ?.disable_user_create_org
  253. ? "true"
  254. : "false";
  255. process.env.RESOURCE_ACCESS_TOKEN_PARAM =
  256. parsedConfig.data.server.resource_access_token_param;
  257. process.env.RESOURCE_SESSION_REQUEST_PARAM =
  258. parsedConfig.data.server.resource_session_request_param;
  259. process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
  260. ?.allow_base_domain_resources
  261. ? "true"
  262. : "false";
  263. this.rawConfig = parsedConfig.data;
  264. }
  265. public getRawConfig() {
  266. return this.rawConfig;
  267. }
  268. public getBaseDomain(): string {
  269. return this.rawConfig.app.base_domain;
  270. }
  271. public getNoReplyEmail(): string | undefined {
  272. return (
  273. this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
  274. );
  275. }
  276. private createTraefikConfig() {
  277. try {
  278. // check if traefik_config.yml and dynamic_config.yml exists in APP_PATH/traefik
  279. const defaultTraefikConfigPath = path.join(
  280. __DIRNAME,
  281. "traefik_config.example.yml"
  282. );
  283. const defaultDynamicConfigPath = path.join(
  284. __DIRNAME,
  285. "dynamic_config.example.yml"
  286. );
  287. const traefikPath = path.join(APP_PATH, "traefik");
  288. if (!fs.existsSync(traefikPath)) {
  289. return;
  290. }
  291. // load default configs
  292. let traefikConfig = fs.readFileSync(
  293. defaultTraefikConfigPath,
  294. "utf8"
  295. );
  296. let dynamicConfig = fs.readFileSync(
  297. defaultDynamicConfigPath,
  298. "utf8"
  299. );
  300. traefikConfig = traefikConfig
  301. .split("{{.LetsEncryptEmail}}")
  302. .join(this.rawConfig.users.server_admin.email);
  303. traefikConfig = traefikConfig
  304. .split("{{.INTERNAL_PORT}}")
  305. .join(this.rawConfig.server.internal_port.toString());
  306. dynamicConfig = dynamicConfig
  307. .split("{{.DashboardDomain}}")
  308. .join(new URL(this.rawConfig.app.dashboard_url).hostname);
  309. dynamicConfig = dynamicConfig
  310. .split("{{.NEXT_PORT}}")
  311. .join(this.rawConfig.server.next_port.toString());
  312. dynamicConfig = dynamicConfig
  313. .split("{{.EXTERNAL_PORT}}")
  314. .join(this.rawConfig.server.external_port.toString());
  315. // write thiese to the traefik directory
  316. const traefikConfigPath = path.join(
  317. traefikPath,
  318. "traefik_config.yml"
  319. );
  320. const dynamicConfigPath = path.join(
  321. traefikPath,
  322. "dynamic_config.yml"
  323. );
  324. fs.writeFileSync(traefikConfigPath, traefikConfig, "utf8");
  325. fs.writeFileSync(dynamicConfigPath, dynamicConfig, "utf8");
  326. console.log("Traefik configuration files created");
  327. } catch (e) {
  328. console.log(
  329. "Failed to generate the Traefik configuration files. Please create them manually."
  330. );
  331. console.error(e);
  332. }
  333. }
  334. }
  335. export const config = new Config();
  336. export default config;