config.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
  7. import { loadAppVersion } from "@server/lib/loadAppVersion";
  8. import { passwordSchema } from "@server/auth/passwordSchema";
  9. const portSchema = z.number().positive().gt(0).lte(65535);
  10. const hostnameSchema = z
  11. .string()
  12. .regex(
  13. /^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
  14. "Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
  15. );
  16. const environmentSchema = z.object({
  17. app: z.object({
  18. dashboard_url: z
  19. .string()
  20. .url()
  21. .transform((url) => url.toLowerCase()),
  22. base_domain: hostnameSchema,
  23. log_level: z.enum(["debug", "info", "warn", "error"]),
  24. save_logs: z.boolean()
  25. }),
  26. server: z.object({
  27. external_port: portSchema,
  28. internal_port: portSchema,
  29. next_port: portSchema,
  30. internal_hostname: z.string().transform((url) => url.toLowerCase()),
  31. secure_cookies: z.boolean(),
  32. session_cookie_name: z.string(),
  33. resource_session_cookie_name: z.string()
  34. }),
  35. traefik: z.object({
  36. http_entrypoint: z.string(),
  37. https_entrypoint: z.string().optional(),
  38. cert_resolver: z.string().optional(),
  39. prefer_wildcard_cert: z.boolean().optional()
  40. }),
  41. gerbil: z.object({
  42. start_port: portSchema,
  43. base_endpoint: z.string().transform((url) => url.toLowerCase()),
  44. use_subdomain: z.boolean(),
  45. subnet_group: z.string(),
  46. block_size: z.number().positive().gt(0)
  47. }),
  48. rate_limits: z.object({
  49. global: z.object({
  50. window_minutes: z.number().positive().gt(0),
  51. max_requests: z.number().positive().gt(0)
  52. }),
  53. auth: z
  54. .object({
  55. window_minutes: z.number().positive().gt(0),
  56. max_requests: z.number().positive().gt(0)
  57. })
  58. .optional()
  59. }),
  60. email: z
  61. .object({
  62. smtp_host: z.string(),
  63. smtp_port: portSchema,
  64. smtp_user: z.string(),
  65. smtp_pass: z.string(),
  66. no_reply: z.string().email()
  67. })
  68. .optional(),
  69. users: z.object({
  70. server_admin: z.object({
  71. email: z.string().email(),
  72. password: passwordSchema
  73. })
  74. }),
  75. flags: z
  76. .object({
  77. require_email_verification: z.boolean().optional(),
  78. disable_signup_without_invite: z.boolean().optional(),
  79. disable_user_create_org: z.boolean().optional()
  80. })
  81. .optional()
  82. });
  83. export class Config {
  84. private rawConfig!: z.infer<typeof environmentSchema>;
  85. constructor() {
  86. this.loadConfig();
  87. }
  88. public loadConfig() {
  89. const loadConfig = (configPath: string) => {
  90. try {
  91. const yamlContent = fs.readFileSync(configPath, "utf8");
  92. const config = yaml.load(yamlContent);
  93. return config;
  94. } catch (error) {
  95. if (error instanceof Error) {
  96. throw new Error(
  97. `Error loading configuration file: ${error.message}`
  98. );
  99. }
  100. throw error;
  101. }
  102. };
  103. let environment: any;
  104. if (fs.existsSync(configFilePath1)) {
  105. environment = loadConfig(configFilePath1);
  106. } else if (fs.existsSync(configFilePath2)) {
  107. environment = loadConfig(configFilePath2);
  108. }
  109. if (!environment) {
  110. const exampleConfigPath = path.join(
  111. __DIRNAME,
  112. "config.example.yml"
  113. );
  114. if (fs.existsSync(exampleConfigPath)) {
  115. try {
  116. const exampleConfigContent = fs.readFileSync(
  117. exampleConfigPath,
  118. "utf8"
  119. );
  120. fs.writeFileSync(
  121. configFilePath1,
  122. exampleConfigContent,
  123. "utf8"
  124. );
  125. environment = loadConfig(configFilePath1);
  126. } catch (error) {
  127. if (error instanceof Error) {
  128. throw new Error(
  129. `Error creating configuration file from example: ${
  130. error.message
  131. }`
  132. );
  133. }
  134. throw error;
  135. }
  136. } else {
  137. throw new Error(
  138. "No configuration file found and no example configuration available"
  139. );
  140. }
  141. }
  142. if (!environment) {
  143. throw new Error("No configuration file found");
  144. }
  145. const parsedConfig = environmentSchema.safeParse(environment);
  146. if (!parsedConfig.success) {
  147. const errors = fromError(parsedConfig.error);
  148. throw new Error(`Invalid configuration file: ${errors}`);
  149. }
  150. const appVersion = loadAppVersion();
  151. if (!appVersion) {
  152. throw new Error("Could not load the application version");
  153. }
  154. process.env.APP_VERSION = appVersion;
  155. process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
  156. process.env.SERVER_EXTERNAL_PORT =
  157. parsedConfig.data.server.external_port.toString();
  158. process.env.SERVER_INTERNAL_PORT =
  159. parsedConfig.data.server.internal_port.toString();
  160. process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
  161. ?.require_email_verification
  162. ? "true"
  163. : "false";
  164. process.env.SESSION_COOKIE_NAME =
  165. parsedConfig.data.server.session_cookie_name;
  166. process.env.RESOURCE_SESSION_COOKIE_NAME =
  167. parsedConfig.data.server.resource_session_cookie_name;
  168. process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
  169. process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
  170. ?.disable_signup_without_invite
  171. ? "true"
  172. : "false";
  173. process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
  174. ?.disable_user_create_org
  175. ? "true"
  176. : "false";
  177. this.rawConfig = parsedConfig.data;
  178. }
  179. public getRawConfig() {
  180. return this.rawConfig;
  181. }
  182. public getBaseDomain(): string {
  183. return this.rawConfig.app.base_domain;
  184. }
  185. }
  186. export const config = new Config();
  187. export default config;