optionally generate traefik files, set cors in config, and set trust proxy in config

This commit is contained in:
Milo Schwartz
2025-01-15 23:26:31 -05:00
parent cb87463a69
commit 1aec431c36
13 changed files with 300 additions and 49 deletions

View File

@@ -20,7 +20,9 @@ const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() {
const apiServer = express();
apiServer.set("trust proxy", 1);
if (config.getRawConfig().server.trust_proxy) {
apiServer.set("trust proxy", 1);
}
const corsConfig = config.getRawConfig().server.cors;

View File

@@ -11,6 +11,7 @@ import {
} from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion";
import { passwordSchema } from "@server/auth/passwordSchema";
import stoi from "./stoi";
const portSchema = z.number().positive().gt(0).lte(65535);
const hostnameSchema = z
@@ -20,31 +21,56 @@ const hostnameSchema = z
)
.or(z.literal("localhost"));
const environmentSchema = z.object({
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml;
};
const configSchema = z.object({
app: z.object({
dashboard_url: z
.string()
.url()
.optional()
.transform(getEnvOrYaml("APP_DASHBOARDURL"))
.pipe(z.string().url())
.transform((url) => url.toLowerCase()),
base_domain: hostnameSchema,
base_domain: hostnameSchema
.optional()
.transform(getEnvOrYaml("APP_BASEDOMAIN"))
.pipe(hostnameSchema),
log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean()
}),
server: z.object({
external_port: portSchema,
internal_port: portSchema,
next_port: portSchema,
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),
internal_hostname: z.string().transform((url) => url.toLowerCase()),
secure_cookies: z.boolean(),
session_cookie_name: z.string(),
resource_session_cookie_name: z.string(),
resource_access_token_param: z.string(),
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()
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(),
@@ -53,8 +79,17 @@ const environmentSchema = z.object({
prefer_wildcard_cert: z.boolean().optional()
}),
gerbil: z.object({
start_port: portSchema,
base_endpoint: z.string().transform((url) => url.toLowerCase()),
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()),
use_subdomain: z.boolean(),
subnet_group: z.string(),
block_size: z.number().positive().gt(0),
@@ -83,8 +118,16 @@ const environmentSchema = z.object({
.optional(),
users: z.object({
server_admin: z.object({
email: z.string().email(),
email: z
.string()
.email()
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
.pipe(z.string().email()),
password: passwordSchema
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
.pipe(passwordSchema)
})
}),
flags: z
@@ -97,12 +140,18 @@ const environmentSchema = z.object({
});
export class Config {
private rawConfig!: z.infer<typeof environmentSchema>;
private rawConfig!: z.infer<typeof configSchema>;
constructor() {
this.loadConfig();
if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
this.createTraefikConfig();
}
}
public loadEnvironment() {}
public loadConfig() {
const loadConfig = (configPath: string) => {
try {
@@ -166,7 +215,7 @@ export class Config {
throw new Error("No configuration file found");
}
const parsedConfig = environmentSchema.safeParse(environment);
const parsedConfig = configSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
@@ -214,6 +263,72 @@ export class Config {
public getBaseDomain(): string {
return this.rawConfig.app.base_domain;
}
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);
}
}
}
export const config = new Config();

View File

@@ -10,6 +10,7 @@ import m1 from "./scripts/1.0.0-beta1";
import m2 from "./scripts/1.0.0-beta2";
import m3 from "./scripts/1.0.0-beta3";
import m4 from "./scripts/1.0.0-beta5";
import m5 from "./scripts/1.0.0-beta6";
import { existsSync, mkdirSync } from "fs";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
@@ -20,7 +21,8 @@ const migrations = [
{ version: "1.0.0-beta.1", run: m1 },
{ version: "1.0.0-beta.2", run: m2 },
{ version: "1.0.0-beta.3", run: m3 },
{ version: "1.0.0-beta.5", run: m4 }
{ version: "1.0.0-beta.5", run: m4 },
{ version: "1.0.0-beta.6", run: m5 }
// Add new migrations here as they are created
] as const;

View File

@@ -0,0 +1,52 @@
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import fs from "fs";
import yaml from "js-yaml";
export default async function migration() {
console.log("Running setup script 1.0.0-beta.6...");
try {
// Determine which config file exists
const filePaths = [configFilePath1, configFilePath2];
let filePath = "";
for (const path of filePaths) {
if (fs.existsSync(path)) {
filePath = path;
break;
}
}
if (!filePath) {
throw new Error(
`No config file found (expected config.yml or config.yaml).`
);
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
rawConfig = yaml.load(fileContents);
// Validate the structure
if (!rawConfig.server) {
throw new Error(`Invalid config file: server is missing.`);
}
// Update the config
rawConfig.server.cors = {
origins: [rawConfig.app.dashboard_url],
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
headers: ["X-CSRF-Token", "Content-Type"],
credentials: false
};
// Write the updated YAML back to the file
const updatedYaml = yaml.dump(rawConfig);
fs.writeFileSync(filePath, updatedYaml, "utf8");
} catch (error) {
console.log("We were unable to add CORS to your config file. Please add it manually.")
console.error(error)
}
console.log("Done.");
}