mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-01 08:34:53 +02:00
remove base_url
from config (#13)
* add example config dir, logos, and update CONTRIBUTING.md * update dockerignore * split base_url into dashboard_url and base_domain * Remove unessicary ports * Allow anything for the ip * Update docker tags * Complex regex for domains/ips * update gitignore --------- Co-authored-by: Owen Schwartz <owen@txv.io>
This commit is contained in:
parent
a36691e5ab
commit
235e91294e
23 changed files with 193 additions and 51 deletions
|
@ -31,7 +31,7 @@ export function createApiServer() {
|
|||
);
|
||||
} else {
|
||||
const corsOptions = {
|
||||
origin: config.getRawConfig().app.base_url,
|
||||
origin: config.getRawConfig().app.dashboard_url,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
allowedHeaders: ["Content-Type", "X-CSRF-Token"]
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export async function sendEmailVerificationCode(
|
|||
VerifyEmail({
|
||||
username: email,
|
||||
verificationCode: code,
|
||||
verifyLink: `${config.getRawConfig().app.base_url}/auth/verify-email`
|
||||
verifyLink: `${config.getRawConfig().app.dashboard_url}/auth/verify-email`
|
||||
}),
|
||||
{
|
||||
to: email,
|
||||
|
|
|
@ -3,18 +3,25 @@ import yaml from "js-yaml";
|
|||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||
import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
const hostnameSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})*$/,
|
||||
"Invalid hostname. Must be a valid hostname like 'localhost' or 'test.example.com'."
|
||||
);
|
||||
|
||||
const environmentSchema = z.object({
|
||||
app: z.object({
|
||||
base_url: z
|
||||
dashboard_url: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((url) => url.toLowerCase()),
|
||||
base_domain: hostnameSchema,
|
||||
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||
save_logs: z.boolean()
|
||||
}),
|
||||
|
@ -58,7 +65,7 @@ const environmentSchema = z.object({
|
|||
smtp_port: portSchema,
|
||||
smtp_user: z.string(),
|
||||
smtp_pass: z.string(),
|
||||
no_reply: z.string().email(),
|
||||
no_reply: z.string().email()
|
||||
})
|
||||
.optional(),
|
||||
users: z.object({
|
||||
|
@ -99,9 +106,6 @@ export class Config {
|
|||
}
|
||||
};
|
||||
|
||||
const configFilePath1 = path.join(APP_PATH, "config.yml");
|
||||
const configFilePath2 = path.join(APP_PATH, "config.yaml");
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
|
@ -190,15 +194,7 @@ export class Config {
|
|||
}
|
||||
|
||||
public getBaseDomain(): string {
|
||||
const newUrl = new URL(this.rawConfig.app.base_url);
|
||||
const hostname = newUrl.hostname;
|
||||
const parts = hostname.split(".");
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
return parts.slice(1).join(".");
|
||||
return this.rawConfig.app.base_domain;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,3 +6,6 @@ export const __FILENAME = fileURLToPath(import.meta.url);
|
|||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
export const APP_PATH = path.join("config");
|
||||
|
||||
export const configFilePath1 = path.join(APP_PATH, "config.yml");
|
||||
export const configFilePath2 = path.join(APP_PATH, "config.yaml");
|
||||
|
|
|
@ -82,7 +82,7 @@ export async function requestPasswordReset(
|
|||
});
|
||||
});
|
||||
|
||||
const url = `${config.getRawConfig().app.base_url}/auth/reset-password?email=${email}&token=${token}`;
|
||||
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
|
||||
|
||||
await sendEmail(
|
||||
ResetPasswordCode({
|
||||
|
|
|
@ -101,7 +101,7 @@ export async function verifyResourceSession(
|
|||
return allowed(res);
|
||||
}
|
||||
|
||||
const redirectUrl = `${config.getRawConfig().app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
if (!sessions) {
|
||||
return notAllowed(res);
|
||||
|
|
|
@ -82,7 +82,6 @@ export async function createOrg(
|
|||
let org: Org | null = null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// create a url from config.getRawConfig().app.base_url and get the hostname
|
||||
const domain = config.getBaseDomain();
|
||||
|
||||
const newOrg = await trx
|
||||
|
|
|
@ -12,6 +12,34 @@ import { isIpInCidr } from "@server/lib/ip";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import { addTargets } from "../newt/targets";
|
||||
|
||||
// Regular expressions for validation
|
||||
const DOMAIN_REGEX =
|
||||
/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
const IPV4_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]?)$/;
|
||||
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
||||
|
||||
// Schema for domain names and IP addresses
|
||||
const domainSchema = z
|
||||
.string()
|
||||
.min(1, "Domain cannot be empty")
|
||||
.max(255, "Domain name too long")
|
||||
.refine(
|
||||
(value) => {
|
||||
// Check if it's a valid IP address (v4 or v6)
|
||||
if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid domain name
|
||||
return DOMAIN_REGEX.test(value);
|
||||
},
|
||||
{
|
||||
message: "Invalid domain name or IP address format",
|
||||
path: ["domain"]
|
||||
}
|
||||
);
|
||||
|
||||
const createTargetParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
|
@ -23,7 +51,7 @@ const createTargetParamsSchema = z
|
|||
|
||||
const createTargetSchema = z
|
||||
.object({
|
||||
ip: z.string().ip().or(z.literal('localhost')),
|
||||
ip: domainSchema,
|
||||
method: z.string().min(1).max(10),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
protocol: z.string().optional(),
|
||||
|
|
|
@ -11,6 +11,34 @@ import { fromError } from "zod-validation-error";
|
|||
import { addPeer } from "../gerbil/peers";
|
||||
import { addTargets } from "../newt/targets";
|
||||
|
||||
// Regular expressions for validation
|
||||
const DOMAIN_REGEX =
|
||||
/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
const IPV4_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]?)$/;
|
||||
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
||||
|
||||
// Schema for domain names and IP addresses
|
||||
const domainSchema = z
|
||||
.string()
|
||||
.min(1, "Domain cannot be empty")
|
||||
.max(255, "Domain name too long")
|
||||
.refine(
|
||||
(value) => {
|
||||
// Check if it's a valid IP address (v4 or v6)
|
||||
if (IPV4_REGEX.test(value) || IPV6_REGEX.test(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid domain name
|
||||
return DOMAIN_REGEX.test(value);
|
||||
},
|
||||
{
|
||||
message: "Invalid domain name or IP address format",
|
||||
path: ["domain"]
|
||||
}
|
||||
);
|
||||
|
||||
const updateTargetParamsSchema = z
|
||||
.object({
|
||||
targetId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||
|
@ -19,7 +47,7 @@ const updateTargetParamsSchema = z
|
|||
|
||||
const updateTargetBodySchema = z
|
||||
.object({
|
||||
ip: z.string().ip().or(z.literal('localhost')).optional(), // for now we cant update the ip; you will have to delete
|
||||
ip: domainSchema.optional(),
|
||||
method: z.string().min(1).max(10).optional(),
|
||||
port: z.number().int().min(1).max(65535).optional(),
|
||||
enabled: z.boolean().optional()
|
||||
|
|
|
@ -152,7 +152,7 @@ export async function inviteUser(
|
|||
});
|
||||
});
|
||||
|
||||
const inviteLink = `${config.getRawConfig().app.base_url}/invite?token=${inviteId}-${token}`;
|
||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`;
|
||||
|
||||
if (doEmail) {
|
||||
await sendEmail(
|
||||
|
|
|
@ -5,7 +5,6 @@ import { eq, ne } from "drizzle-orm";
|
|||
import logger from "@server/logger";
|
||||
|
||||
export async function copyInConfig() {
|
||||
// create a url from config.getRawConfig().app.base_url and get the hostname
|
||||
const domain = config.getBaseDomain();
|
||||
const endpoint = config.getRawConfig().gerbil.base_endpoint;
|
||||
|
||||
|
|
|
@ -7,13 +7,15 @@ import { desc } from "drizzle-orm";
|
|||
import { __DIRNAME } from "@server/lib/consts";
|
||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||
import m1 from "./scripts/1.0.0-beta1";
|
||||
import m2 from "./scripts/1.0.0-beta2";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
|
||||
// Define the migration list with versions and their corresponding functions
|
||||
const migrations = [
|
||||
{ version: "1.0.0-beta.1", run: m1 }
|
||||
{ version: "1.0.0-beta.1", run: m1 },
|
||||
{ version: "1.0.0-beta.2", run: m2 }
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import logger from "@server/logger";
|
||||
|
||||
export default async function migration() {
|
||||
console.log("Running setup script 1.0.0-beta.1");
|
||||
console.log("Running setup script 1.0.0-beta.1...");
|
||||
// SQL operations would go here in ts format
|
||||
console.log("Done...");
|
||||
console.log("Done.");
|
||||
}
|
||||
|
|
59
server/setup/scripts/1.0.0-beta2.ts
Normal file
59
server/setup/scripts/1.0.0-beta2.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
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.2...");
|
||||
|
||||
// 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.app || !rawConfig.app.base_url) {
|
||||
throw new Error(`Invalid config file: app.base_url is missing.`);
|
||||
}
|
||||
|
||||
// Move base_url to dashboard_url and calculate base_domain
|
||||
const baseUrl = rawConfig.app.base_url;
|
||||
rawConfig.app.dashboard_url = baseUrl;
|
||||
rawConfig.app.base_domain = getBaseDomain(baseUrl);
|
||||
|
||||
// Remove the old base_url
|
||||
delete rawConfig.app.base_url;
|
||||
|
||||
// Write the updated YAML back to the file
|
||||
const updatedYaml = yaml.dump(rawConfig);
|
||||
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
function getBaseDomain(url: string): string {
|
||||
const newUrl = new URL(url);
|
||||
const hostname = newUrl.hostname;
|
||||
const parts = hostname.split(".");
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
return parts.slice(-2).join(".");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue