remove environment variable support and config file autogeneration

This commit is contained in:
miloschwartz 2025-03-08 18:05:53 -05:00
parent 9253dd19ba
commit c93b36c757
No known key found for this signature in database
6 changed files with 17 additions and 271 deletions

View file

@ -26,7 +26,6 @@ COPY --from=builder /app/.next ./.next
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init COPY --from=builder /app/init ./dist/init
COPY config/config.example.yml ./dist/config.example.yml
COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml
COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml
COPY server/db/names.json ./dist/names.json COPY server/db/names.json ./dist/names.json

View file

@ -1,53 +0,0 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`{{.DashboardDomain}}`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# API router (handles /api/v1 paths)
api-router:
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`{{.DashboardDomain}}`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:{{.NEXT_PORT}}" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:{{.EXTERNAL_PORT}}" # API/WebSocket server

View file

@ -1,44 +0,0 @@
api:
insecure: true
dashboard: true
providers:
http:
endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.0.0-beta.3"
log:
level: "INFO"
format: "common"
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true

View file

@ -1,11 +1,9 @@
import fs from "fs"; import fs from "fs";
import yaml from "js-yaml"; import yaml from "js-yaml";
import path from "path";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import {
__DIRNAME, __DIRNAME,
APP_PATH,
APP_VERSION, APP_VERSION,
configFilePath1, configFilePath1,
configFilePath2 configFilePath2
@ -14,12 +12,6 @@ import { passwordSchema } from "@server/auth/passwordSchema";
import stoi from "./stoi"; import stoi from "./stoi";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
// const hostnameSchema = z
// .string()
// .regex(
// /^(?:(?: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])$/
// )
// .or(z.literal("localhost"));
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml; return process.env[envVar] ?? valFromYaml;
@ -31,7 +23,6 @@ const configSchema = z.object({
.string() .string()
.url() .url()
.optional() .optional()
.transform(getEnvOrYaml("APP_DASHBOARDURL"))
.pipe(z.string().url()) .pipe(z.string().url())
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
log_level: z.enum(["debug", "info", "warn", "error"]), log_level: z.enum(["debug", "info", "warn", "error"]),
@ -63,37 +54,11 @@ const configSchema = z.object({
{ {
message: "At least one domain must be defined" message: "At least one domain must be defined"
} }
)
.refine(
(domains) => {
const envBaseDomain = process.env.APP_BASE_DOMAIN;
if (envBaseDomain) {
return z.string().nonempty().safeParse(envBaseDomain).success;
}
return true;
},
{
message: "APP_BASE_DOMAIN must be a valid hostname"
}
), ),
server: z.object({ server: z.object({
external_port: portSchema external_port: portSchema.optional().transform(stoi).pipe(portSchema),
.optional() internal_port: portSchema.optional().transform(stoi).pipe(portSchema),
.transform(getEnvOrYaml("SERVER_EXTERNALPORT")) next_port: portSchema.optional().transform(stoi).pipe(portSchema),
.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()), internal_hostname: z.string().transform((url) => url.toLowerCase()),
session_cookie_name: z.string(), session_cookie_name: z.string(),
resource_access_token_param: z.string(), resource_access_token_param: z.string(),
@ -126,15 +91,10 @@ const configSchema = z.object({
additional_middlewares: z.array(z.string()).optional() additional_middlewares: z.array(z.string()).optional()
}), }),
gerbil: z.object({ gerbil: z.object({
start_port: portSchema start_port: portSchema.optional().transform(stoi).pipe(portSchema),
.optional()
.transform(getEnvOrYaml("GERBIL_STARTPORT"))
.transform(stoi)
.pipe(portSchema),
base_endpoint: z base_endpoint: z
.string() .string()
.optional() .optional()
.transform(getEnvOrYaml("GERBIL_BASEENDPOINT"))
.pipe(z.string()) .pipe(z.string())
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
use_subdomain: z.boolean(), use_subdomain: z.boolean(),
@ -197,10 +157,6 @@ export class Config {
constructor() { constructor() {
this.loadConfig(); this.loadConfig();
if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
this.createTraefikConfig();
}
} }
public loadConfig() { public loadConfig() {
@ -226,44 +182,9 @@ export class Config {
environment = loadConfig(configFilePath2); environment = loadConfig(configFilePath2);
} }
if (!environment) { if (!environment) {
const exampleConfigPath = path.join( throw new Error(
__DIRNAME, "No configuration file found. Please create one. https://docs.fossorial.io/"
"config.example.yml"
); );
if (fs.existsSync(exampleConfigPath)) {
try {
const exampleConfigContent = fs.readFileSync(
exampleConfigPath,
"utf8"
);
fs.writeFileSync(
configFilePath1,
exampleConfigContent,
"utf8"
);
environment = loadConfig(configFilePath1);
} catch (error) {
console.log(
"See the docs for information about what to include in the configuration file: https://docs.fossorial.io/Pangolin/Configuration/config"
);
if (error instanceof Error) {
throw new Error(
`Error creating configuration file from example: ${
error.message
}`
);
}
throw error;
}
} else {
throw new Error(
"No configuration file found and no example configuration available"
);
}
}
if (!environment) {
throw new Error("No configuration file found");
} }
const parsedConfig = configSchema.safeParse(environment); const parsedConfig = configSchema.safeParse(environment);
@ -309,17 +230,6 @@ export class Config {
: "false"; : "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
if (process.env.APP_BASE_DOMAIN) {
console.log(
`DEPRECATED! APP_BASE_DOMAIN is deprecated and will be removed in a future release. Use the domains section in the configuration file instead. See https://docs.fossorial.io/Pangolin/Configuration/config for more information.`
);
parsedConfig.data.domains.domain1 = {
base_domain: process.env.APP_BASE_DOMAIN,
cert_resolver: "letsencrypt"
};
}
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig.data;
} }
@ -336,72 +246,6 @@ export class Config {
public getDomain(domainId: string) { public getDomain(domainId: string) {
return this.rawConfig.domains[domainId]; return this.rawConfig.domains[domainId];
} }
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(); export const config = new Config();

View file

@ -18,7 +18,7 @@ export async function clearStaleData() {
.delete(sessions) .delete(sessions)
.where(lt(sessions.expiresAt, new Date().getTime())); .where(lt(sessions.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired sessions:", e); logger.warn("Error clearing expired sessions:", e);
} }
try { try {
@ -26,7 +26,7 @@ export async function clearStaleData() {
.delete(newtSessions) .delete(newtSessions)
.where(lt(newtSessions.expiresAt, new Date().getTime())); .where(lt(newtSessions.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired newtSessions:", e); logger.warn("Error clearing expired newtSessions:", e);
} }
try { try {
@ -34,7 +34,7 @@ export async function clearStaleData() {
.delete(emailVerificationCodes) .delete(emailVerificationCodes)
.where(lt(emailVerificationCodes.expiresAt, new Date().getTime())); .where(lt(emailVerificationCodes.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired emailVerificationCodes:", e); logger.warn("Error clearing expired emailVerificationCodes:", e);
} }
try { try {
@ -42,7 +42,7 @@ export async function clearStaleData() {
.delete(passwordResetTokens) .delete(passwordResetTokens)
.where(lt(passwordResetTokens.expiresAt, new Date().getTime())); .where(lt(passwordResetTokens.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired passwordResetTokens:", e); logger.warn("Error clearing expired passwordResetTokens:", e);
} }
try { try {
@ -50,7 +50,7 @@ export async function clearStaleData() {
.delete(userInvites) .delete(userInvites)
.where(lt(userInvites.expiresAt, new Date().getTime())); .where(lt(userInvites.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired userInvites:", e); logger.warn("Error clearing expired userInvites:", e);
} }
try { try {
@ -58,7 +58,7 @@ export async function clearStaleData() {
.delete(resourceAccessToken) .delete(resourceAccessToken)
.where(lt(resourceAccessToken.expiresAt, new Date().getTime())); .where(lt(resourceAccessToken.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired resourceAccessToken:", e); logger.warn("Error clearing expired resourceAccessToken:", e);
} }
try { try {
@ -66,7 +66,7 @@ export async function clearStaleData() {
.delete(resourceSessions) .delete(resourceSessions)
.where(lt(resourceSessions.expiresAt, new Date().getTime())); .where(lt(resourceSessions.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired resourceSessions:", e); logger.warn("Error clearing expired resourceSessions:", e);
} }
try { try {
@ -74,6 +74,6 @@ export async function clearStaleData() {
.delete(resourceOtp) .delete(resourceOtp)
.where(lt(resourceOtp.expiresAt, new Date().getTime())); .where(lt(resourceOtp.expiresAt, new Date().getTime()));
} catch (e) { } catch (e) {
logger.error("Error clearing expired resourceOtp:", e); logger.warn("Error clearing expired resourceOtp:", e);
} }
} }

View file

@ -40,9 +40,6 @@ const migrations = [
await run(); await run();
async function run() { async function run() {
// backup the database
backupDb();
// run the migrations // run the migrations
await runMigrations(); await runMigrations();
} }
@ -127,6 +124,9 @@ async function executeScripts() {
console.log(`Running migration ${migration.version}`); console.log(`Running migration ${migration.version}`);
try { try {
// Backup the database before running the migration
backupDb();
await migration.run(); await migration.run();
// Update version in database // Update version in database