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 config, { APP_PATH } from "@server/config";
import path from "path";
export default defineConfig({

View file

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

View file

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

View file

@ -12,12 +12,11 @@ import { eq } from "drizzle-orm";
import config from "@server/config";
import type { RandomReader } 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 SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export function generateSessionToken(): string {
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 { eq } from "drizzle-orm";
import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "session";
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export async function createNewtSession(
token: string,

View file

@ -8,12 +8,11 @@ import {
import db from "@server/db";
import { eq, and } from "drizzle-orm";
import config from "@server/config";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export const SESSION_COOKIE_NAME = "resource_session";
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
export const SECURE_COOKIES = config.server.secure_cookies;
export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url);
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
export async function createResourceSession(opts: {
token: string;

View file

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

View file

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

View file

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

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 Database from "better-sqlite3";
import * as schema from "@server/db/schema";
import { APP_PATH } from "@server/config";
import path from "path";
import fs from "fs/promises";
import { APP_PATH } from "@server/consts";
export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = await checkFileExists(location);
@ -20,4 +20,4 @@ async function checkFileExists(filePath: string): Promise<boolean> {
} catch {
return false;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ export const verifySessionUserMiddleware = async (
if (
!existingUser[0].emailVerified &&
config.flags?.require_email_verification
config.getRawConfig().flags?.require_email_verification
) {
return next(
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 config from "@server/config";
const nextPort = config.server.next_port;
const nextPort = config.getRawConfig().server.next_port;
export async function createNextServer() {
// const app = next({ dev });

View file

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

View file

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

View file

@ -16,7 +16,7 @@ export async function requestEmailVerificationCode(
res: Response,
next: NextFunction
): Promise<any> {
if (!config.flags?.require_email_verification) {
if (!config.getRawConfig().flags?.require_email_verification) {
return next(
createHttpError(
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(
ResetPasswordCode({
@ -91,7 +91,7 @@ export async function requestPasswordReset(
link: url
}),
{
from: config.email?.no_reply,
from: config.getRawConfig().email?.no_reply,
to: email,
subject: "Reset your password"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -423,11 +423,11 @@ unauthenticated.use("/auth", authRouter);
authRouter.use(
rateLimitMiddleware({
windowMin:
config.rate_limits.auth?.window_minutes ||
config.rate_limits.global.window_minutes,
config.getRawConfig().rate_limits.auth?.window_minutes ||
config.getRawConfig().rate_limits.global.window_minutes,
max:
config.rate_limits.auth?.max_requests ||
config.rate_limits.global.max_requests,
config.getRawConfig().rate_limits.auth?.max_requests ||
config.getRawConfig().rate_limits.global.max_requests,
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 listenPort = await getNextAvailablePort();
let subEndpoint = "";
if (config.gerbil.use_subdomain) {
if (config.getRawConfig().gerbil.use_subdomain) {
subEndpoint = await getUniqueExitNodeEndpointName();
}
// create a new exit node
exitNode = await db.insert(exitNodes).values({
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.gerbil.base_endpoint}`,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
listenPort,
reachableAt,
@ -122,7 +122,7 @@ async function getNextAvailableSubnet(): Promise<string> {
}).from(exitNodes);
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) {
throw new Error('No available subnets remaining in space');
}
@ -139,7 +139,7 @@ async function getNextAvailablePort(): Promise<number> {
}).from(exitNodes);
// 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) {
if (port.listenPort > nextPort) {
break;

View file

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

View file

@ -134,7 +134,7 @@ export async function authWithAccessToken(
expiresAt: tokenItem.expiresAt,
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);
res.appendHeader("Set-Cookie", cookie);

View file

@ -122,7 +122,7 @@ export async function authWithPassword(
token,
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);
res.appendHeader("Set-Cookie", cookie);

View file

@ -133,7 +133,7 @@ export async function authWithPincode(
token,
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);
res.appendHeader("Set-Cookie", cookie);

View file

@ -177,7 +177,7 @@ export async function authWithWhitelist(
token,
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);
res.appendHeader("Set-Cookie", cookie);

View file

@ -50,12 +50,12 @@ export async function traefikConfigProvider(
[badgerMiddlewareName]: {
apiBaseUrl: new URL(
"/api/v1",
`http://${config.server.internal_hostname}:${config.server.internal_port}`,
`http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
).href,
resourceSessionCookieName:
config.server.resource_session_cookie_name,
config.getRawConfig().server.resource_session_cookie_name,
userSessionCookieName:
config.server.session_cookie_name,
config.getRawConfig().server.session_cookie_name,
},
},
},
@ -95,8 +95,8 @@ export async function traefikConfigProvider(
}
const tls = {
certResolver: config.traefik.cert_resolver,
...(config.traefik.prefer_wildcard_cert
certResolver: config.getRawConfig().traefik.cert_resolver,
...(config.getRawConfig().traefik.prefer_wildcard_cert
? {
domains: [
{
@ -110,8 +110,8 @@ export async function traefikConfigProvider(
http.routers![routerName] = {
entryPoints: [
resource.ssl
? config.traefik.https_entrypoint
: config.traefik.http_entrypoint,
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint,
],
middlewares: [badgerMiddlewareName],
service: serviceName,
@ -122,7 +122,7 @@ export async function traefikConfigProvider(
if (resource.ssl) {
// this is a redirect router; all it does is redirect to the https version if tls is enabled
http.routers![routerName + "-redirect"] = {
entryPoints: [config.traefik.http_entrypoint],
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
middlewares: [redirectMiddlewareName],
service: serviceName,
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) {
await sendEmail(
@ -165,7 +165,7 @@ export async function inviteUser(
}),
{
to: email,
from: config.email?.no_reply,
from: config.getRawConfig().email?.no_reply,
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 { ne } from "drizzle-orm";
import logger from "@server/logger";
import { extractBaseDomain } from "@server/utils/extractBaseDomain";
export async function copyInConfig() {
// create a url from config.app.base_url and get the hostname
const domain = extractBaseDomain(config.app.base_url);
// create a url from config.getRawConfig().app.base_url and get the hostname
const domain = config.getBaseDomain();
// 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

View file

@ -1,23 +1,15 @@
import { ensureActions } from "./ensureActions";
import { copyInConfig } from "./copyInConfig";
import { runMigrations } from "./migrations";
import { setupServerAdmin } from "./setupServerAdmin";
import { loadConfig } from "@server/config";
import logger from "@server/logger";
export async function runSetupFunctions() {
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 setupServerAdmin();
await ensureActions(); // make sure all of the actions are in the db and the roles
} catch (error) {
console.error("Error running setup functions:", error);
logger.error("Error running setup functions:", error);
process.exit(1);
}
}

View file

@ -1,14 +1,15 @@
import { __DIRNAME } from "@server/config";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db, { exists } from "@server/db";
import path from "path";
import semver from "semver";
import { versionMigrations } from "@server/db/schema";
import { desc } from "drizzle-orm";
// Import all migrations explicitly
import { __DIRNAME } from "@server/consts";
import { loadAppVersion } from "@server/utils/loadAppVersion";
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
const migrations = [
@ -16,34 +17,32 @@ const migrations = [
// Add new migrations here as they are created
] as const;
export async function runMigrations() {
if (!process.env.APP_VERSION) {
throw new Error("APP_VERSION is not set in the environment");
}
// Run the migrations
await runMigrations();
if (process.env.ENVIRONMENT !== "prod") {
console.info("Skipping migrations in non-prod environment");
return;
export async function runMigrations() {
const appVersion = loadAppVersion();
if (!appVersion) {
throw new Error("APP_VERSION is not set in the environment");
}
if (exists) {
await executeScripts();
} else {
console.info("Running migrations...");
console.log("Running migrations...");
try {
migrate(db, {
migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
});
console.info("Migrations completed successfully.");
console.log("Migrations completed successfully.");
} catch (error) {
console.error("Error running migrations:", error);
}
// insert process.env.APP_VERSION into the versionMigrations table
await db
.insert(versionMigrations)
.values({
version: process.env.APP_VERSION,
version: appVersion,
executedAt: Date.now()
})
.execute();
@ -60,7 +59,7 @@ async function executeScripts() {
.limit(1);
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
const pendingMigrations = migrations
@ -69,7 +68,7 @@ async function executeScripts() {
// Run migrations in order
for (const migration of pendingMigrations) {
console.info(`Running migration ${migration.version}`);
console.log(`Running migration ${migration.version}`);
try {
await migration.run();
@ -83,7 +82,7 @@ async function executeScripts() {
})
.execute();
console.info(
console.log(
`Successfully completed migration ${migration.version}`
);
} catch (error) {
@ -95,7 +94,7 @@ async function executeScripts() {
}
}
console.info("All migrations completed successfully");
console.log("All migrations completed successfully");
} catch (error) {
console.error("Migration process failed:", error);
throw error;

View file

@ -1,7 +1,7 @@
import logger from "@server/logger";
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
logger.info("Done...");
console.log("Done...");
}

View file

@ -12,7 +12,7 @@ import { fromError } from "zod-validation-error";
export async function setupServerAdmin() {
const {
server_admin: { email, password }
} = config.users;
} = config.getRawConfig().users;
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 });
if (!authInfo) {
{/* @ts-ignore */} // TODO: fix this
return (
<div className="w-full max-w-md">
<ResourceNotFound />

View file

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