config.ts 6.7 KB

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