make config class and separate migrations script

This commit is contained in:
Milo Schwartz 2025-01-01 17:50:12 -05:00
parent b199595100
commit 9732098799
No known key found for this signature in database
45 changed files with 163 additions and 156 deletions

View file

@ -1,5 +1,5 @@
import { APP_PATH } from "@server/consts";
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
import config, { APP_PATH } from "@server/config";
import path from "path"; import path from "path";
export default defineConfig({ export default defineConfig({

View file

@ -8,8 +8,8 @@
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:push": "npx tsx server/db/migrate.ts", "db:push": "npx tsx server/db/migrate.ts",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs", "build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs",
"start": "NODE_ENV=development ENVIRONMENT=prod NODE_OPTIONS=--enable-source-maps node dist/server.mjs", "start": "NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
"email": "email dev --dir server/emails/templates --port 3005" "email": "email dev --dir server/emails/templates --port 3005"
}, },
"dependencies": { "dependencies": {

View file

@ -15,7 +15,7 @@ import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod"; const dev = process.env.ENVIRONMENT !== "prod";
const externalPort = config.server.external_port; const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {
const apiServer = express(); const apiServer = express();
@ -25,13 +25,13 @@ export function createApiServer() {
if (dev) { if (dev) {
apiServer.use( apiServer.use(
cors({ cors({
origin: `http://localhost:${config.server.next_port}`, origin: `http://localhost:${config.getRawConfig().server.next_port}`,
credentials: true credentials: true
}) })
); );
} else { } else {
const corsOptions = { const corsOptions = {
origin: config.app.base_url, origin: config.getRawConfig().app.base_url,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "X-CSRF-Token"] allowedHeaders: ["Content-Type", "X-CSRF-Token"]
}; };
@ -47,8 +47,8 @@ export function createApiServer() {
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: config.rate_limits.global.window_minutes, windowMin: config.getRawConfig().rate_limits.global.window_minutes,
max: config.rate_limits.global.max_requests, max: config.getRawConfig().rate_limits.global.max_requests,
type: "IP_AND_PATH" type: "IP_AND_PATH"
}) })
); );

View file

@ -12,12 +12,11 @@ import { eq } from "drizzle-orm";
import config from "@server/config"; import config from "@server/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 { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = config.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.server.secure_cookies; export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export function generateSessionToken(): string { export function generateSessionToken(): string {
const bytes = new Uint8Array(20); const bytes = new Uint8Array(20);

View file

@ -9,12 +9,11 @@ import { Newt, newts, newtSessions, NewtSession } from "@server/db/schema";
import db from "@server/db"; import db from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import config from "@server/config"; import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "session"; export const SESSION_COOKIE_NAME = "session";
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.server.secure_cookies; export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export async function createNewtSession( export async function createNewtSession(
token: string, token: string,

View file

@ -8,12 +8,11 @@ import {
import db from "@server/db"; import db from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/config"; import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "resource_session"; export const SESSION_COOKIE_NAME = "resource_session";
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.server.secure_cookies; export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export async function createResourceSession(opts: { export async function createResourceSession(opts: {
token: string; token: string;

View file

@ -26,7 +26,7 @@ export async function sendResourceOtpEmail(
}), }),
{ {
to: email, to: email,
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
subject: `Your one-time code to access ${resourceName}` subject: `Your one-time code to access ${resourceName}`
} }
); );

View file

@ -17,11 +17,11 @@ export async function sendEmailVerificationCode(
VerifyEmail({ VerifyEmail({
username: email, username: email,
verificationCode: code, verificationCode: code,
verifyLink: `${config.app.base_url}/auth/verify-email` verifyLink: `${config.getRawConfig().app.base_url}/auth/verify-email`
}), }),
{ {
to: email, to: email,
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
subject: "Verify your email address" subject: "Verify your email address"
} }
); );

View file

@ -1,14 +1,10 @@
import fs from "fs"; import fs from "fs";
import yaml from "js-yaml"; import yaml from "js-yaml";
import path from "path"; import path from "path";
import { fileURLToPath } from "url";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { __DIRNAME, APP_PATH } from "@server/consts";
export const __FILENAME = fileURLToPath(import.meta.url); import { loadAppVersion } from "@server/utils/loadAppVersion";
export const __DIRNAME = path.dirname(__FILENAME);
export const APP_PATH = path.join("config");
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
@ -86,10 +82,6 @@ export class Config {
this.loadConfig(); this.loadConfig();
} }
public getRawConfig() {
return this.rawConfig;
}
public loadConfig() { public loadConfig() {
const loadConfig = (configPath: string) => { const loadConfig = (configPath: string) => {
try { try {
@ -160,16 +152,11 @@ export class Config {
throw new Error(`Invalid configuration file: ${errors}`); throw new Error(`Invalid configuration file: ${errors}`);
} }
const packageJsonPath = path.join(__DIRNAME, "..", "package.json"); const appVersion = loadAppVersion();
let packageJson: any; if (!appVersion) {
if (fs.existsSync && fs.existsSync(packageJsonPath)) { throw new Error("Could not load the application version");
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
packageJson = JSON.parse(packageJsonContent);
if (packageJson.version) {
process.env.APP_VERSION = packageJson.version;
}
} }
process.env.APP_VERSION = appVersion;
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT = process.env.SERVER_EXTERNAL_PORT =
@ -196,6 +183,22 @@ export class Config {
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig.data;
} }
public getRawConfig() {
return this.rawConfig;
}
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(".");
}
} }
export const config = new Config(); export const config = new Config();

8
server/consts.ts Normal file
View file

@ -0,0 +1,8 @@
import path from "path";
import { fileURLToPath } from "url";
import { existsSync } from "fs";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
export const APP_PATH = path.join("config");

View file

@ -1,9 +1,9 @@
import { drizzle } from "drizzle-orm/better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import * as schema from "@server/db/schema"; import * as schema from "@server/db/schema";
import { APP_PATH } from "@server/config";
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs/promises";
import { APP_PATH } from "@server/consts";
export const location = path.join(APP_PATH, "db", "db.sqlite"); export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = await checkFileExists(location); export const exists = await checkFileExists(location);

View file

@ -3,7 +3,7 @@ import { readFileSync } from "fs";
import { db } from "@server/db"; import { db } from "@server/db";
import { exitNodes, sites } from "./schema"; import { exitNodes, sites } from "./schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { __DIRNAME } from "@server/config"; import { __DIRNAME } from "@server/consts";
// Load the names from the names.json file // Load the names from the names.json file
const dev = process.env.ENVIRONMENT !== "prod"; const dev = process.env.ENVIRONMENT !== "prod";

View file

@ -5,25 +5,26 @@ import config from "@server/config";
import logger from "@server/logger"; import logger from "@server/logger";
function createEmailClient() { function createEmailClient() {
if ( const emailConfig = config.getRawConfig().email;
!config.email?.smtp_host || if (
!config.email?.smtp_pass || !emailConfig?.smtp_host ||
!config.email?.smtp_port || !emailConfig?.smtp_pass ||
!config.email?.smtp_user !emailConfig?.smtp_port ||
) { !emailConfig?.smtp_user
logger.warn( ) {
"Email SMTP configuration is missing. Emails will not be sent.", logger.warn(
); "Email SMTP configuration is missing. Emails will not be sent.",
return; );
} return;
}
return nodemailer.createTransport({ return nodemailer.createTransport({
host: config.email.smtp_host, host: emailConfig.smtp_host,
port: config.email.smtp_port, port: emailConfig.smtp_port,
secure: false, secure: false,
auth: { auth: {
user: config.email.smtp_user, user: emailConfig.smtp_user,
pass: config.email.smtp_pass, pass: emailConfig.smtp_pass,
}, },
}); });
} }

View file

@ -1,8 +1,8 @@
import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer"; import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer"; import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { User, UserOrg } from "./db/schema"; import { User, UserOrg } from "./db/schema";
import { runSetupFunctions } from "./setup";
async function startServers() { async function startServers() {
await runSetupFunctions(); await runSetupFunctions();

View file

@ -10,7 +10,7 @@ import {
} from "@server/middlewares"; } from "@server/middlewares";
import internal from "@server/routers/internal"; import internal from "@server/routers/internal";
const internalPort = config.server.internal_port; const internalPort = config.getRawConfig().server.internal_port;
export function createInternalServer() { export function createInternalServer() {
const internalServer = express(); const internalServer = express();

View file

@ -1,7 +1,8 @@
import "winston-daily-rotate-file"; import "winston-daily-rotate-file";
import config, { APP_PATH } from "@server/config"; import config from "@server/config";
import * as winston from "winston"; import * as winston from "winston";
import path from "path"; import path from "path";
import { APP_PATH } from "./consts";
const hformat = winston.format.printf( const hformat = winston.format.printf(
({ level, label, message, timestamp, stack, ...metadata }) => { ({ level, label, message, timestamp, stack, ...metadata }) => {
@ -18,7 +19,7 @@ const hformat = winston.format.printf(
const transports: any = [new winston.transports.Console({})]; const transports: any = [new winston.transports.Console({})];
if (config.app.save_logs) { if (config.getRawConfig().app.save_logs) {
transports.push( transports.push(
new winston.transports.DailyRotateFile({ new winston.transports.DailyRotateFile({
filename: path.join(APP_PATH, "logs", "pangolin-%DATE%.log"), filename: path.join(APP_PATH, "logs", "pangolin-%DATE%.log"),
@ -49,7 +50,7 @@ if (config.app.save_logs) {
} }
const logger = winston.createLogger({ const logger = winston.createLogger({
level: config.app.log_level.toLowerCase(), level: config.getRawConfig().app.log_level.toLowerCase(),
format: winston.format.combine( format: winston.format.combine(
winston.format.errors({ stack: true }), winston.format.errors({ stack: true }),
winston.format.colorize(), winston.format.colorize(),

View file

@ -34,7 +34,7 @@ export const verifySessionUserMiddleware = async (
if ( if (
!existingUser[0].emailVerified && !existingUser[0].emailVerified &&
config.flags?.require_email_verification config.getRawConfig().flags?.require_email_verification
) { ) {
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "Email is not verified") // Might need to change the response type? createHttpError(HttpCode.BAD_REQUEST, "Email is not verified") // Might need to change the response type?

View file

@ -4,7 +4,7 @@ import { parse } from "url";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/config"; import config from "@server/config";
const nextPort = config.server.next_port; const nextPort = config.getRawConfig().server.next_port;
export async function createNextServer() { export async function createNextServer() {
// const app = next({ dev }); // const app = next({ dev });

View file

@ -99,7 +99,7 @@ export async function disable2fa(
}), }),
{ {
to: user.email, to: user.email,
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
subject: "Two-factor authentication disabled" subject: "Two-factor authentication disabled"
} }
); );

View file

@ -127,7 +127,7 @@ export async function login(
if ( if (
!existingUser.emailVerified && !existingUser.emailVerified &&
config.flags?.require_email_verification config.getRawConfig().flags?.require_email_verification
) { ) {
return response<LoginResponse>(res, { return response<LoginResponse>(res, {
data: { emailVerificationRequired: true }, data: { emailVerificationRequired: true },

View file

@ -16,7 +16,7 @@ export async function requestEmailVerificationCode(
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
if (!config.flags?.require_email_verification) { if (!config.getRawConfig().flags?.require_email_verification) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -82,7 +82,7 @@ export async function requestPasswordReset(
}); });
}); });
const url = `${config.app.base_url}/auth/reset-password?email=${email}&token=${token}`; const url = `${config.getRawConfig().app.base_url}/auth/reset-password?email=${email}&token=${token}`;
await sendEmail( await sendEmail(
ResetPasswordCode({ ResetPasswordCode({
@ -91,7 +91,7 @@ export async function requestPasswordReset(
link: url link: url
}), }),
{ {
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
to: email, to: email,
subject: "Reset your password" subject: "Reset your password"
} }

View file

@ -147,7 +147,7 @@ export async function resetPassword(
}); });
await sendEmail(ConfirmPasswordReset({ email }), { await sendEmail(ConfirmPasswordReset({ email }), {
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
to: email, to: email,
subject: "Password Reset Confirmation" subject: "Password Reset Confirmation"
}); });

View file

@ -60,7 +60,7 @@ export async function signup(
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
const userId = generateId(15); const userId = generateId(15);
if (config.flags?.disable_signup_without_invite) { if (config.getRawConfig().flags?.disable_signup_without_invite) {
if (!inviteToken || !inviteId) { if (!inviteToken || !inviteId) {
return next( return next(
createHttpError( createHttpError(
@ -102,7 +102,7 @@ export async function signup(
.where(eq(users.email, email)); .where(eq(users.email, email));
if (existing && existing.length > 0) { if (existing && existing.length > 0) {
if (!config.flags?.require_email_verification) { if (!config.getRawConfig().flags?.require_email_verification) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -163,7 +163,7 @@ export async function signup(
const cookie = serializeSessionCookie(token); const cookie = serializeSessionCookie(token);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
if (config.flags?.require_email_verification) { if (config.getRawConfig().flags?.require_email_verification) {
sendEmailVerificationCode(email, userId); sendEmailVerificationCode(email, userId);
return response<SignUpResponse>(res, { return response<SignUpResponse>(res, {

View file

@ -28,7 +28,7 @@ export async function verifyEmail(
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
if (!config.flags?.require_email_verification) { if (!config.getRawConfig().flags?.require_email_verification) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,

View file

@ -111,7 +111,7 @@ export async function verifyTotp(
}), }),
{ {
to: user.email, to: user.email,
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
subject: "Two-factor authentication enabled" subject: "Two-factor authentication enabled"
} }
); );

View file

@ -101,13 +101,13 @@ export async function verifyResourceSession(
return allowed(res); return allowed(res);
} }
const redirectUrl = `${config.app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; const redirectUrl = `${config.getRawConfig().app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
if (!sessions) { if (!sessions) {
return notAllowed(res); return notAllowed(res);
} }
const sessionToken = sessions[config.server.session_cookie_name]; const sessionToken = sessions[config.getRawConfig().server.session_cookie_name];
// check for unified login // check for unified login
if (sso && sessionToken) { if (sso && sessionToken) {
@ -129,7 +129,7 @@ export async function verifyResourceSession(
const resourceSessionToken = const resourceSessionToken =
sessions[ sessions[
`${config.server.resource_session_cookie_name}_${resource.resourceId}` `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`
]; ];
if (resourceSessionToken) { if (resourceSessionToken) {
@ -213,7 +213,7 @@ async function isUserAllowedToAccessResource(
user: User, user: User,
resource: Resource resource: Resource
): Promise<boolean> { ): Promise<boolean> {
if (config.flags?.require_email_verification && !user.emailVerified) { if (config.getRawConfig().flags?.require_email_verification && !user.emailVerified) {
return false; return false;
} }

View file

@ -423,11 +423,11 @@ unauthenticated.use("/auth", authRouter);
authRouter.use( authRouter.use(
rateLimitMiddleware({ rateLimitMiddleware({
windowMin: windowMin:
config.rate_limits.auth?.window_minutes || config.getRawConfig().rate_limits.auth?.window_minutes ||
config.rate_limits.global.window_minutes, config.getRawConfig().rate_limits.global.window_minutes,
max: max:
config.rate_limits.auth?.max_requests || config.getRawConfig().rate_limits.auth?.max_requests ||
config.rate_limits.global.max_requests, config.getRawConfig().rate_limits.global.max_requests,
type: "IP_AND_PATH" type: "IP_AND_PATH"
}) })
); );

View file

@ -52,14 +52,14 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
const address = await getNextAvailableSubnet(); const address = await getNextAvailableSubnet();
const listenPort = await getNextAvailablePort(); const listenPort = await getNextAvailablePort();
let subEndpoint = ""; let subEndpoint = "";
if (config.gerbil.use_subdomain) { if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName(); subEndpoint = await getUniqueExitNodeEndpointName();
} }
// create a new exit node // create a new exit node
exitNode = await db.insert(exitNodes).values({ exitNode = await db.insert(exitNodes).values({
publicKey, publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.gerbil.base_endpoint}`, endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address, address,
listenPort, listenPort,
reachableAt, reachableAt,
@ -122,7 +122,7 @@ async function getNextAvailableSubnet(): Promise<string> {
}).from(exitNodes); }).from(exitNodes);
const addresses = existingAddresses.map(a => a.address); const addresses = existingAddresses.map(a => a.address);
let subnet = findNextAvailableCidr(addresses, config.gerbil.block_size, config.gerbil.subnet_group); let subnet = findNextAvailableCidr(addresses, config.getRawConfig().gerbil.block_size, config.getRawConfig().gerbil.subnet_group);
if (!subnet) { if (!subnet) {
throw new Error('No available subnets remaining in space'); throw new Error('No available subnets remaining in space');
} }
@ -139,7 +139,7 @@ async function getNextAvailablePort(): Promise<number> {
}).from(exitNodes); }).from(exitNodes);
// Find the first available port between 1024 and 65535 // Find the first available port between 1024 and 65535
let nextPort = config.gerbil.start_port; let nextPort = config.getRawConfig().gerbil.start_port;
for (const port of existingPorts) { for (const port of existingPorts) {
if (port.listenPort > nextPort) { if (port.listenPort > nextPort) {
break; break;

View file

@ -11,7 +11,6 @@ import { createAdminRole } from "@server/setup/ensureActions";
import config from "@server/config"; import config from "@server/config";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { defaultRoleAllowedActions } from "../role"; import { defaultRoleAllowedActions } from "../role";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
const createOrgSchema = z const createOrgSchema = z
.object({ .object({
@ -30,7 +29,7 @@ export async function createOrg(
): Promise<any> { ): Promise<any> {
try { try {
// should this be in a middleware? // should this be in a middleware?
if (config.flags?.disable_user_create_org) { if (config.getRawConfig().flags?.disable_user_create_org) {
if (!req.user?.serverAdmin) { if (!req.user?.serverAdmin) {
return next( return next(
createHttpError( createHttpError(
@ -83,8 +82,8 @@ export async function createOrg(
let org: Org | null = null; let org: Org | null = null;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// create a url from config.app.base_url and get the hostname // create a url from config.getRawConfig().app.base_url and get the hostname
const domain = extractBaseDomain(config.app.base_url); const domain = config.getBaseDomain();
const newOrg = await trx const newOrg = await trx
.insert(orgs) .insert(orgs)

View file

@ -134,7 +134,7 @@ export async function authWithAccessToken(
expiresAt: tokenItem.expiresAt, expiresAt: tokenItem.expiresAt,
doNotExtend: tokenItem.expiresAt ? true : false doNotExtend: tokenItem.expiresAt ? true : false
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie(cookieName, token); const cookie = serializeResourceSessionCookie(cookieName, token);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -122,7 +122,7 @@ export async function authWithPassword(
token, token,
passwordId: definedPassword.passwordId passwordId: definedPassword.passwordId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie(cookieName, token); const cookie = serializeResourceSessionCookie(cookieName, token);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -133,7 +133,7 @@ export async function authWithPincode(
token, token,
pincodeId: definedPincode.pincodeId pincodeId: definedPincode.pincodeId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie(cookieName, token); const cookie = serializeResourceSessionCookie(cookieName, token);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -177,7 +177,7 @@ export async function authWithWhitelist(
token, token,
whitelistId: whitelistedEmail.whitelistId whitelistId: whitelistedEmail.whitelistId
}); });
const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
const cookie = serializeResourceSessionCookie(cookieName, token); const cookie = serializeResourceSessionCookie(cookieName, token);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -50,12 +50,12 @@ export async function traefikConfigProvider(
[badgerMiddlewareName]: { [badgerMiddlewareName]: {
apiBaseUrl: new URL( apiBaseUrl: new URL(
"/api/v1", "/api/v1",
`http://${config.server.internal_hostname}:${config.server.internal_port}`, `http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
).href, ).href,
resourceSessionCookieName: resourceSessionCookieName:
config.server.resource_session_cookie_name, config.getRawConfig().server.resource_session_cookie_name,
userSessionCookieName: userSessionCookieName:
config.server.session_cookie_name, config.getRawConfig().server.session_cookie_name,
}, },
}, },
}, },
@ -95,8 +95,8 @@ export async function traefikConfigProvider(
} }
const tls = { const tls = {
certResolver: config.traefik.cert_resolver, certResolver: config.getRawConfig().traefik.cert_resolver,
...(config.traefik.prefer_wildcard_cert ...(config.getRawConfig().traefik.prefer_wildcard_cert
? { ? {
domains: [ domains: [
{ {
@ -110,8 +110,8 @@ export async function traefikConfigProvider(
http.routers![routerName] = { http.routers![routerName] = {
entryPoints: [ entryPoints: [
resource.ssl resource.ssl
? config.traefik.https_entrypoint ? config.getRawConfig().traefik.https_entrypoint
: config.traefik.http_entrypoint, : config.getRawConfig().traefik.http_entrypoint,
], ],
middlewares: [badgerMiddlewareName], middlewares: [badgerMiddlewareName],
service: serviceName, service: serviceName,
@ -122,7 +122,7 @@ export async function traefikConfigProvider(
if (resource.ssl) { if (resource.ssl) {
// this is a redirect router; all it does is redirect to the https version if tls is enabled // this is a redirect router; all it does is redirect to the https version if tls is enabled
http.routers![routerName + "-redirect"] = { http.routers![routerName + "-redirect"] = {
entryPoints: [config.traefik.http_entrypoint], entryPoints: [config.getRawConfig().traefik.http_entrypoint],
middlewares: [redirectMiddlewareName], middlewares: [redirectMiddlewareName],
service: serviceName, service: serviceName,
rule: `Host(\`${fullDomain}\`)`, rule: `Host(\`${fullDomain}\`)`,

View file

@ -152,7 +152,7 @@ export async function inviteUser(
}); });
}); });
const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`; const inviteLink = `${config.getRawConfig().app.base_url}/invite?token=${inviteId}-${token}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(
@ -165,7 +165,7 @@ export async function inviteUser(
}), }),
{ {
to: email, to: email,
from: config.email?.no_reply, from: config.getRawConfig().email?.no_reply,
subject: "You're invited to join a Fossorial organization" subject: "You're invited to join a Fossorial organization"
} }
); );

View file

@ -3,11 +3,10 @@ import { orgs } from "../db/schema";
import config from "@server/config"; import config from "@server/config";
import { ne } from "drizzle-orm"; import { ne } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export async function copyInConfig() { export async function copyInConfig() {
// create a url from config.app.base_url and get the hostname // create a url from config.getRawConfig().app.base_url and get the hostname
const domain = extractBaseDomain(config.app.base_url); const domain = config.getBaseDomain();
// 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

View file

@ -1,23 +1,15 @@
import { ensureActions } from "./ensureActions"; import { ensureActions } from "./ensureActions";
import { copyInConfig } from "./copyInConfig"; import { copyInConfig } from "./copyInConfig";
import { runMigrations } from "./migrations";
import { setupServerAdmin } from "./setupServerAdmin"; import { setupServerAdmin } from "./setupServerAdmin";
import { loadConfig } from "@server/config"; import logger from "@server/logger";
export async function runSetupFunctions() { export async function runSetupFunctions() {
try { try {
await runMigrations(); // run the migrations
console.log("Migrations completed successfully.")
// ANYTHING BEFORE THIS LINE CANNOT USE THE CONFIG
loadConfig();
await copyInConfig(); // copy in the config to the db as needed await copyInConfig(); // copy in the config to the db as needed
await setupServerAdmin(); await setupServerAdmin();
await ensureActions(); // make sure all of the actions are in the db and the roles await ensureActions(); // make sure all of the actions are in the db and the roles
} catch (error) { } catch (error) {
console.error("Error running setup functions:", error); logger.error("Error running setup functions:", error);
process.exit(1); process.exit(1);
} }
} }

View file

@ -1,14 +1,15 @@
import { __DIRNAME } from "@server/config";
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db, { exists } from "@server/db"; import db, { exists } from "@server/db";
import path from "path"; import path from "path";
import semver from "semver"; import semver from "semver";
import { versionMigrations } from "@server/db/schema"; import { versionMigrations } from "@server/db/schema";
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import { __DIRNAME } from "@server/consts";
// Import all migrations explicitly import { loadAppVersion } from "@server/utils/loadAppVersion";
import m1 from "./scripts/1.0.0-beta1"; import m1 from "./scripts/1.0.0-beta1";
// Add new migration imports here as they are created
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
// Define the migration list with versions and their corresponding functions // Define the migration list with versions and their corresponding functions
const migrations = [ const migrations = [
@ -16,34 +17,32 @@ const migrations = [
// Add new migrations here as they are created // Add new migrations here as they are created
] as const; ] as const;
export async function runMigrations() { // Run the migrations
if (!process.env.APP_VERSION) { await runMigrations();
throw new Error("APP_VERSION is not set in the environment");
}
if (process.env.ENVIRONMENT !== "prod") { export async function runMigrations() {
console.info("Skipping migrations in non-prod environment"); const appVersion = loadAppVersion();
return; if (!appVersion) {
throw new Error("APP_VERSION is not set in the environment");
} }
if (exists) { if (exists) {
await executeScripts(); await executeScripts();
} else { } else {
console.info("Running migrations..."); console.log("Running migrations...");
try { try {
migrate(db, { migrate(db, {
migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
}); });
console.info("Migrations completed successfully."); console.log("Migrations completed successfully.");
} catch (error) { } catch (error) {
console.error("Error running migrations:", error); console.error("Error running migrations:", error);
} }
// insert process.env.APP_VERSION into the versionMigrations table
await db await db
.insert(versionMigrations) .insert(versionMigrations)
.values({ .values({
version: process.env.APP_VERSION, version: appVersion,
executedAt: Date.now() executedAt: Date.now()
}) })
.execute(); .execute();
@ -60,7 +59,7 @@ async function executeScripts() {
.limit(1); .limit(1);
const startVersion = lastExecuted[0]?.version ?? "0.0.0"; const startVersion = lastExecuted[0]?.version ?? "0.0.0";
console.info(`Starting migrations from version ${startVersion}`); console.log(`Starting migrations from version ${startVersion}`);
// Filter and sort migrations // Filter and sort migrations
const pendingMigrations = migrations const pendingMigrations = migrations
@ -69,7 +68,7 @@ async function executeScripts() {
// Run migrations in order // Run migrations in order
for (const migration of pendingMigrations) { for (const migration of pendingMigrations) {
console.info(`Running migration ${migration.version}`); console.log(`Running migration ${migration.version}`);
try { try {
await migration.run(); await migration.run();
@ -83,7 +82,7 @@ async function executeScripts() {
}) })
.execute(); .execute();
console.info( console.log(
`Successfully completed migration ${migration.version}` `Successfully completed migration ${migration.version}`
); );
} catch (error) { } catch (error) {
@ -95,7 +94,7 @@ async function executeScripts() {
} }
} }
console.info("All migrations completed successfully"); console.log("All migrations completed successfully");
} catch (error) { } catch (error) {
console.error("Migration process failed:", error); console.error("Migration process failed:", error);
throw error; throw error;

View file

@ -1,7 +1,7 @@
import logger from "@server/logger"; import logger from "@server/logger";
export default async function migration() { export default async function migration() {
logger.info("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 // SQL operations would go here in ts format
logger.info("Done..."); console.log("Done...");
} }

View file

@ -12,7 +12,7 @@ import { fromError } from "zod-validation-error";
export async function setupServerAdmin() { export async function setupServerAdmin() {
const { const {
server_admin: { email, password } server_admin: { email, password }
} = config.users; } = config.getRawConfig().users;
const parsed = passwordSchema.safeParse(password); const parsed = passwordSchema.safeParse(password);

View file

@ -1,11 +0,0 @@
export function extractBaseDomain(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(1).join(".");
}

View file

@ -0,0 +1,16 @@
import path from "path";
import { __DIRNAME } from "@server/consts";
import fs from "fs";
export function loadAppVersion() {
const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
let packageJson: any;
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
packageJson = JSON.parse(packageJsonContent);
if (packageJson.version) {
return packageJson.version;
}
}
}

View file

@ -42,6 +42,7 @@ export default async function ResourceAuthPage(props: {
const user = await getUser({ skipCheckVerifyEmail: true }); const user = await getUser({ skipCheckVerifyEmail: true });
if (!authInfo) { if (!authInfo) {
{/* @ts-ignore */} // TODO: fix this
return ( return (
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<ResourceNotFound /> <ResourceNotFound />

View file

@ -48,17 +48,19 @@ export function Header({ orgId, orgs }: HeaderProps) {
<div className="hidden md:block"> <div className="hidden md:block">
<div className="flex items-center gap-4 mr-4"> <div className="flex items-center gap-4 mr-4">
<Link <Link
href="/docs" href="https://docs.fossorial.io"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
> >
Documentation Documentation
</Link> </Link>
<Link <a
href="/support" href="mailto:support@fossorial.io"
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
> >
Support Support
</Link> </a>
</div> </div>
</div> </div>