Merge pull request #58 from fosrl/dev

various changes to to allow for unraid deployment
This commit is contained in:
Milo Schwartz 2025-01-15 23:52:49 -05:00 committed by GitHub
commit e4fe749251
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 384 additions and 64 deletions

View file

@ -27,6 +27,8 @@ 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/config.example.yml ./dist/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 server/db/names.json ./dist/names.json COPY server/db/names.json ./dist/names.json
COPY public ./public COPY public ./public

View file

@ -1,15 +1,15 @@
app: app:
dashboard_url: http://localhost dashboard_url: http://localhost:3002
base_domain: localhost base_domain: localhost
log_level: debug log_level: info
save_logs: false save_logs: false
server: server:
external_port: 3000 external_port: 3000
internal_port: 3001 internal_port: 3001
next_port: 3002 next_port: 3002
internal_hostname: localhost internal_hostname: pangolin
secure_cookies: false secure_cookies: true
session_cookie_name: p_session session_cookie_name: p_session
resource_session_cookie_name: p_resource_session resource_session_cookie_name: p_resource_session
resource_access_token_param: p_token resource_access_token_param: p_token
@ -39,3 +39,5 @@ users:
flags: flags:
require_email_verification: false require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: true

View file

@ -0,0 +1,54 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
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

@ -0,0 +1,41 @@
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.2"
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"
http:
tls:
certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true

View file

@ -13,6 +13,11 @@ server:
session_cookie_name: p_session session_cookie_name: p_session
resource_session_cookie_name: p_resource_session resource_session_cookie_name: p_resource_session
resource_access_token_param: p_token resource_access_token_param: p_token
cors:
origins: ["https://{{.DashboardDomain}}"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
traefik: traefik:
cert_resolver: letsencrypt cert_resolver: letsencrypt

View file

@ -18,8 +18,8 @@ import (
) )
func loadVersions(config *Config) { func loadVersions(config *Config) {
config.PangolinVersion = "1.0.0-beta.5" config.PangolinVersion = "1.0.0-beta.6"
config.GerbilVersion = "1.0.0-beta.1" config.GerbilVersion = "1.0.0-beta.2"
} }
//go:embed fs/* //go:embed fs/*

View file

@ -1,6 +1,6 @@
{ {
"name": "@fosrl/pangolin", "name": "@fosrl/pangolin",
"version": "1.0.0-beta.5", "version": "1.0.0-beta.6",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI", "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",

View file

@ -20,23 +20,32 @@ const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {
const apiServer = express(); const apiServer = express();
// Middleware setup if (config.getRawConfig().server.trust_proxy) {
apiServer.set("trust proxy", 1); apiServer.set("trust proxy", 1);
if (dev) { }
apiServer.use(
cors({
origin: `http://localhost:${config.getRawConfig().server.next_port}`,
credentials: true
})
);
} else {
const corsOptions = {
origin: config.getRawConfig().app.dashboard_url,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "X-CSRF-Token"]
};
apiServer.use(cors(corsOptions)); const corsConfig = config.getRawConfig().server.cors;
const options = {
...(corsConfig?.origins
? { origin: corsConfig.origins }
: {
origin: (origin: any, callback: any) => {
callback(null, true);
}
}),
...(corsConfig?.methods && { methods: corsConfig.methods }),
...(corsConfig?.allowed_headers && {
allowedHeaders: corsConfig.allowed_headers
}),
credentials: !(corsConfig?.credentials === false)
};
logger.debug("Using CORS options", options);
apiServer.use(cors(options));
if (!dev) {
apiServer.use(helmet()); apiServer.use(helmet());
apiServer.use(csrfProtectionMiddleware); apiServer.use(csrfProtectionMiddleware);
} }
@ -47,7 +56,8 @@ export function createApiServer() {
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: config.getRawConfig().rate_limits.global.window_minutes, windowMin:
config.getRawConfig().rate_limits.global.window_minutes,
max: config.getRawConfig().rate_limits.global.max_requests, max: config.getRawConfig().rate_limits.global.max_requests,
type: "IP_AND_PATH" type: "IP_AND_PATH"
}) })

View file

@ -1,6 +1,6 @@
import { import {
encodeBase32LowerCaseNoPadding, encodeBase32LowerCaseNoPadding,
encodeHexLowerCase, encodeHexLowerCase
} from "@oslojs/encoding"; } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { Session, sessions, User, users } from "@server/db/schema"; import { Session, sessions, User, users } from "@server/db/schema";
@ -9,8 +9,10 @@ import { eq } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import type { RandomReader } from "@oslojs/crypto/random"; import type { RandomReader } from "@oslojs/crypto/random";
import { generateRandomString } from "@oslojs/crypto/random"; import { generateRandomString } from "@oslojs/crypto/random";
import logger from "@server/logger";
export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name;
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies; export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + config.getBaseDomain(); export const COOKIE_DOMAIN = "." + config.getBaseDomain();
@ -24,25 +26,25 @@ export function generateSessionToken(): string {
export async function createSession( export async function createSession(
token: string, token: string,
userId: string, userId: string
): Promise<Session> { ): Promise<Session> {
const sessionId = encodeHexLowerCase( const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)), sha256(new TextEncoder().encode(token))
); );
const session: Session = { const session: Session = {
sessionId: sessionId, sessionId: sessionId,
userId, userId,
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(), expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
}; };
await db.insert(sessions).values(session); await db.insert(sessions).values(session);
return session; return session;
} }
export async function validateSessionToken( export async function validateSessionToken(
token: string, token: string
): Promise<SessionValidationResult> { ): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase( const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)), sha256(new TextEncoder().encode(token))
); );
const result = await db const result = await db
.select({ user: users, session: sessions }) .select({ user: users, session: sessions })
@ -61,12 +63,12 @@ export async function validateSessionToken(
} }
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) { if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
session.expiresAt = new Date( session.expiresAt = new Date(
Date.now() + SESSION_COOKIE_EXPIRES, Date.now() + SESSION_COOKIE_EXPIRES
).getTime(); ).getTime();
await db await db
.update(sessions) .update(sessions)
.set({ .set({
expiresAt: session.expiresAt, expiresAt: session.expiresAt
}) })
.where(eq(sessions.sessionId, session.sessionId)); .where(eq(sessions.sessionId, session.sessionId));
} }
@ -81,26 +83,38 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
await db.delete(sessions).where(eq(sessions.userId, userId)); await db.delete(sessions).where(eq(sessions.userId, userId));
} }
export function serializeSessionCookie(token: string): string { export function serializeSessionCookie(
if (SECURE_COOKIES) { token: string,
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; isSecure: boolean
): string {
if (isSecure) {
logger.debug("Setting cookie for secure origin");
if (SECURE_COOKIES) {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
}
} else { } else {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
} }
} }
export function createBlankSessionTokenCookie(): string { export function createBlankSessionTokenCookie(isSecure: boolean): string {
if (SECURE_COOKIES) { if (isSecure) {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; if (SECURE_COOKIES) {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
}
} else { } else {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
} }
} }
const random: RandomReader = { const random: RandomReader = {
read(bytes: Uint8Array): void { read(bytes: Uint8Array): void {
crypto.getRandomValues(bytes); crypto.getRandomValues(bytes);
}, }
}; };
export function generateId(length: number): string { export function generateId(length: number): string {

View file

@ -3,9 +3,15 @@ import yaml from "js-yaml";
import path from "path"; 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 { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; import {
__DIRNAME,
APP_PATH,
configFilePath1,
configFilePath2
} from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion"; import { loadAppVersion } from "@server/lib/loadAppVersion";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
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 const hostnameSchema = z
@ -15,25 +21,56 @@ const hostnameSchema = z
) )
.or(z.literal("localhost")); .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({ app: z.object({
dashboard_url: z dashboard_url: z
.string() .string()
.url() .url()
.optional()
.transform(getEnvOrYaml("APP_DASHBOARDURL"))
.pipe(z.string().url())
.transform((url) => url.toLowerCase()), .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"]), log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean() save_logs: z.boolean()
}), }),
server: z.object({ server: z.object({
external_port: portSchema, external_port: portSchema
internal_port: portSchema, .optional()
next_port: portSchema, .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()), internal_hostname: z.string().transform((url) => url.toLowerCase()),
secure_cookies: z.boolean(), secure_cookies: z.boolean(),
session_cookie_name: z.string(), session_cookie_name: z.string(),
resource_session_cookie_name: z.string(), resource_session_cookie_name: z.string(),
resource_access_token_param: 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(),
trust_proxy: z.boolean().optional().default(true)
}), }),
traefik: z.object({ traefik: z.object({
http_entrypoint: z.string(), http_entrypoint: z.string(),
@ -42,8 +79,17 @@ const environmentSchema = z.object({
prefer_wildcard_cert: z.boolean().optional() prefer_wildcard_cert: z.boolean().optional()
}), }),
gerbil: z.object({ gerbil: z.object({
start_port: portSchema, start_port: portSchema
base_endpoint: z.string().transform((url) => url.toLowerCase()), .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(), use_subdomain: z.boolean(),
subnet_group: z.string(), subnet_group: z.string(),
block_size: z.number().positive().gt(0), block_size: z.number().positive().gt(0),
@ -72,8 +118,16 @@ const environmentSchema = z.object({
.optional(), .optional(),
users: z.object({ users: z.object({
server_admin: 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 password: passwordSchema
.optional()
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
.pipe(passwordSchema)
}) })
}), }),
flags: z flags: z
@ -86,12 +140,18 @@ const environmentSchema = z.object({
}); });
export class Config { export class Config {
private rawConfig!: z.infer<typeof environmentSchema>; private rawConfig!: z.infer<typeof configSchema>;
constructor() { constructor() {
this.loadConfig(); this.loadConfig();
if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
this.createTraefikConfig();
}
} }
public loadEnvironment() {}
public loadConfig() { public loadConfig() {
const loadConfig = (configPath: string) => { const loadConfig = (configPath: string) => {
try { try {
@ -132,6 +192,9 @@ export class Config {
); );
environment = loadConfig(configFilePath1); environment = loadConfig(configFilePath1);
} catch (error) { } 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) { if (error instanceof Error) {
throw new Error( throw new Error(
`Error creating configuration file from example: ${ `Error creating configuration file from example: ${
@ -152,7 +215,7 @@ export class Config {
throw new Error("No configuration file found"); throw new Error("No configuration file found");
} }
const parsedConfig = environmentSchema.safeParse(environment); const parsedConfig = configSchema.safeParse(environment);
if (!parsedConfig.success) { if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error); const errors = fromError(parsedConfig.error);
@ -187,7 +250,8 @@ export class Config {
?.disable_user_create_org ?.disable_user_create_org
? "true" ? "true"
: "false"; : "false";
process.env.RESOURCE_ACCESS_TOKEN_PARAM = parsedConfig.data.server.resource_access_token_param; process.env.RESOURCE_ACCESS_TOKEN_PARAM =
parsedConfig.data.server.resource_access_token_param;
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig.data;
} }
@ -199,6 +263,72 @@ export class Config {
public getBaseDomain(): string { public getBaseDomain(): string {
return this.rawConfig.app.base_domain; 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(); export const config = new Config();

View file

@ -120,7 +120,8 @@ export async function login(
const token = generateSessionToken(); const token = generateSessionToken();
await createSession(token, existingUser.userId); await createSession(token, existingUser.userId);
const cookie = serializeSessionCookie(token); const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(token, isSecure);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -27,7 +27,8 @@ export async function logout(
try { try {
await invalidateSession(sessionId); await invalidateSession(sessionId);
res.setHeader("Set-Cookie", createBlankSessionTokenCookie()); const isSecure = req.protocol === "https";
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
return response<null>(res, { return response<null>(res, {
data: null, data: null,

View file

@ -158,7 +158,8 @@ export async function signup(
const token = generateSessionToken(); const token = generateSessionToken();
await createSession(token, userId); await createSession(token, userId);
const cookie = serializeSessionCookie(token); const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(token, isSecure);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
if (config.getRawConfig().flags?.require_email_verification) { if (config.getRawConfig().flags?.require_email_verification) {

View file

@ -50,7 +50,9 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
let exitNode; let exitNode;
if (exitNodeQuery.length === 0) { if (exitNodeQuery.length === 0) {
const address = await getNextAvailableSubnet(); const address = await getNextAvailableSubnet();
const listenPort = await getNextAvailablePort(); // TODO: eventually we will want to get the next available port so that we can multiple exit nodes
// const listenPort = await getNextAvailablePort();
const listenPort = config.getRawConfig().gerbil.start_port;
let subEndpoint = ""; let subEndpoint = "";
if (config.getRawConfig().gerbil.use_subdomain) { if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName(); subEndpoint = await getUniqueExitNodeEndpointName();

View file

@ -7,6 +7,7 @@ import logger from "@server/logger";
export async function copyInConfig() { export async function copyInConfig() {
const domain = config.getBaseDomain(); const domain = config.getBaseDomain();
const endpoint = config.getRawConfig().gerbil.base_endpoint; const endpoint = config.getRawConfig().gerbil.base_endpoint;
const listenPort = config.getRawConfig().gerbil.start_port;
// update the domain on all of the orgs where the domain is not equal to the new domain // update the domain on all of the orgs where the domain is not equal to the new domain
// TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary // TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary
@ -14,6 +15,8 @@ export async function copyInConfig() {
// TODO: eventually each exit node could have a different endpoint // TODO: eventually each exit node could have a different endpoint
await db.update(exitNodes).set({ endpoint }).where(ne(exitNodes.endpoint, endpoint)); await db.update(exitNodes).set({ endpoint }).where(ne(exitNodes.endpoint, endpoint));
// TODO: eventually each exit node could have a different port
await db.update(exitNodes).set({ listenPort }).where(ne(exitNodes.listenPort, listenPort));
// update all resources fullDomain to use the new domain // update all resources fullDomain to use the new domain
await db.transaction(async (trx) => { await db.transaction(async (trx) => {

View file

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

View file

@ -235,10 +235,10 @@ PersistentKeepalive = 5`
: ""; : "";
// am I at http or https? // am I at http or https?
let proto = "http:"; let proto = "https:";
if (typeof window !== "undefined") { // if (typeof window !== "undefined") {
proto = window.location.protocol; // proto = window.location.protocol;
} // }
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`; const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;