mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-12 21:30:35 +01:00
make config class and separate migrations script
This commit is contained in:
parent
b199595100
commit
9732098799
45 changed files with 163 additions and 156 deletions
|
@ -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({
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
8
server/consts.ts
Normal 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");
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
});
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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}\`)`,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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...");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(".");
|
||||
}
|
16
server/utils/loadAppVersion.ts
Normal file
16
server/utils/loadAppVersion.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue