diff --git a/server/auth/index.ts b/server/auth/index.ts index d5279aaf..e8c86a01 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -12,12 +12,12 @@ 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_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SECURE_COOKIES = config.server.secure_cookies; -export const COOKIE_DOMAIN = - "." + new URL(config.app.base_url).hostname.split(".").slice(-2).join("."); +export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export function generateSessionToken(): string { const bytes = new Uint8Array(20); diff --git a/server/auth/newt.ts b/server/auth/newt.ts index 84ec0394..97a0b4e2 100644 --- a/server/auth/newt.ts +++ b/server/auth/newt.ts @@ -9,12 +9,12 @@ 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 = - "." + new URL(config.app.base_url).hostname.split(".").slice(-2).join("."); +export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export async function createNewtSession( token: string, diff --git a/server/auth/resource.ts b/server/auth/resource.ts index cc20a022..ef45d4b8 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -8,12 +8,12 @@ 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 = - "." + new URL(config.app.base_url).hostname.split(".").slice(-2).join("."); +export const COOKIE_DOMAIN = "." + extractBaseDomain(config.app.base_url); export async function createResourceSession(opts: { token: string; diff --git a/server/config.ts b/server/config.ts index fe1f863e..b2c99971 100644 --- a/server/config.ts +++ b/server/config.ts @@ -1,9 +1,9 @@ -import { z } from "zod"; -import { fromError } from "zod-validation-error"; -import path from "path"; 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); @@ -79,102 +79,125 @@ const environmentSchema = z.object({ .optional() }); -export function getConfig() { - const loadConfig = (configPath: string) => { - try { - const yamlContent = fs.readFileSync(configPath, "utf8"); - const config = yaml.load(yamlContent); - return config; - } catch (error) { - if (error instanceof Error) { - throw new Error( - `Error loading configuration file: ${error.message}` - ); - } - throw error; - } - }; +export class Config { + private rawConfig!: z.infer; - const configFilePath1 = path.join(APP_PATH, "config.yml"); - const configFilePath2 = path.join(APP_PATH, "config.yaml"); - - let environment: any; - if (fs.existsSync(configFilePath1)) { - environment = loadConfig(configFilePath1); - } else if (fs.existsSync(configFilePath2)) { - environment = loadConfig(configFilePath2); + constructor() { + this.loadConfig(); } - if (!environment) { - const exampleConfigPath = path.join(__DIRNAME, "config.example.yml"); - if (fs.existsSync(exampleConfigPath)) { + + public getRawConfig() { + return this.rawConfig; + } + + public loadConfig() { + const loadConfig = (configPath: string) => { try { - const exampleConfigContent = fs.readFileSync( - exampleConfigPath, - "utf8" - ); - fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8"); - environment = loadConfig(configFilePath1); + const yamlContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(yamlContent); + return config; } catch (error) { if (error instanceof Error) { throw new Error( - `Error creating configuration file from example: ${error.message}` + `Error loading configuration file: ${error.message}` ); } throw error; } - } else { - throw new Error( - "No configuration file found and no example configuration available" + }; + + const configFilePath1 = path.join(APP_PATH, "config.yml"); + const configFilePath2 = path.join(APP_PATH, "config.yaml"); + + let environment: any; + if (fs.existsSync(configFilePath1)) { + environment = loadConfig(configFilePath1); + } else if (fs.existsSync(configFilePath2)) { + environment = loadConfig(configFilePath2); + } + if (!environment) { + const exampleConfigPath = path.join( + __DIRNAME, + "config.example.yml" ); + if (fs.existsSync(exampleConfigPath)) { + try { + const exampleConfigContent = fs.readFileSync( + exampleConfigPath, + "utf8" + ); + fs.writeFileSync( + configFilePath1, + exampleConfigContent, + "utf8" + ); + environment = loadConfig(configFilePath1); + } catch (error) { + if (error instanceof Error) { + throw new Error( + `Error creating configuration file from example: ${ + error.message + }` + ); + } + throw error; + } + } else { + throw new Error( + "No configuration file found and no example configuration available" + ); + } } - } - if (!environment) { - throw new Error("No configuration file found"); - } - - const parsedConfig = environmentSchema.safeParse(environment); - - if (!parsedConfig.success) { - const errors = fromError(parsedConfig.error); - 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; + if (!environment) { + throw new Error("No configuration file found"); } + + const parsedConfig = environmentSchema.safeParse(environment); + + if (!parsedConfig.success) { + const errors = fromError(parsedConfig.error); + 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; + } + } + + process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); + process.env.SERVER_EXTERNAL_PORT = + parsedConfig.data.server.external_port.toString(); + process.env.SERVER_INTERNAL_PORT = + parsedConfig.data.server.internal_port.toString(); + process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags + ?.require_email_verification + ? "true" + : "false"; + process.env.SESSION_COOKIE_NAME = + parsedConfig.data.server.session_cookie_name; + process.env.RESOURCE_SESSION_COOKIE_NAME = + parsedConfig.data.server.resource_session_cookie_name; + process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false"; + process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags + ?.disable_signup_without_invite + ? "true" + : "false"; + process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags + ?.disable_user_create_org + ? "true" + : "false"; + + this.rawConfig = parsedConfig.data; } - - process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); - process.env.SERVER_EXTERNAL_PORT = - parsedConfig.data.server.external_port.toString(); - process.env.SERVER_INTERNAL_PORT = - parsedConfig.data.server.internal_port.toString(); - process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags - ?.require_email_verification - ? "true" - : "false"; - process.env.SESSION_COOKIE_NAME = - parsedConfig.data.server.session_cookie_name; - process.env.RESOURCE_SESSION_COOKIE_NAME = - parsedConfig.data.server.resource_session_cookie_name; - process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false"; - process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags - ?.disable_signup_without_invite - ? "true" - : "false"; - process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags - ?.disable_user_create_org - ? "true" - : "false"; - - return parsedConfig.data; } -export default getConfig(); +export const config = new Config(); + +export default config; diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index e64f0df4..9a993b24 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -6,7 +6,7 @@ import logger from "@server/logger"; export async function sendEmail( template: ReactElement, opts: { - name: string | undefined; + name?: string; from: string | undefined; to: string | undefined; subject: string; diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 9d717979..ea0b01d5 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -11,6 +11,7 @@ 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({ @@ -83,7 +84,7 @@ export async function createOrg( await db.transaction(async (trx) => { // create a url from config.app.base_url and get the hostname - const domain = new URL(config.app.base_url).hostname; + const domain = extractBaseDomain(config.app.base_url); const newOrg = await trx .insert(orgs) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 5ce49ad5..d5b349eb 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -53,8 +53,6 @@ export async function createResource( let { name, subdomain } = parsedBody.data; - subdomain = subdomain.toLowerCase(); // always to lower case - // Validate request params const parsedParams = createResourceParamsSchema.safeParse(req.params); if (!parsedParams.success) { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index b73bc8f7..d8624c82 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -41,8 +41,6 @@ export async function traefikConfigProvider( const badgerMiddlewareName = "badger"; const redirectMiddlewareName = "redirect-to-https"; - // const baseDomain = new URL(config.app.base_url).hostname; - const http: any = { routers: {}, services: {}, diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index 790166c7..68e44c4a 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -3,10 +3,11 @@ 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 = new URL(config.app.base_url).hostname; + const domain = extractBaseDomain(config.app.base_url); // 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 diff --git a/server/setup/index.ts b/server/setup/index.ts index 92d9ea05..ebc40c6e 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -1,18 +1,23 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; -import logger from "@server/logger"; import { runMigrations } from "./migrations"; import { setupServerAdmin } from "./setupServerAdmin"; +import { loadConfig } from "@server/config"; export async function runSetupFunctions() { try { - logger.info(`Setup for version ${process.env.APP_VERSION}`); 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) { - logger.error("Error running setup functions", error); + console.error("Error running setup functions:", error); process.exit(1); } } diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 829c1df4..2883ed27 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -1,4 +1,3 @@ -import logger from "@server/logger"; import { __DIRNAME } from "@server/config"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import db, { exists } from "@server/db"; @@ -8,12 +7,12 @@ import { versionMigrations } from "@server/db/schema"; import { desc } from "drizzle-orm"; // Import all migrations explicitly -import migration100beta1 from "./scripts/1.0.0-beta1"; +import m1 from "./scripts/1.0.0-beta1"; // Add new migration imports here as they are created // Define the migration list with versions and their corresponding functions const migrations = [ - { version: "1.0.0-beta.1", run: migration100beta1 } + { version: "1.0.0-beta.1", run: m1 } // Add new migrations here as they are created ] as const; @@ -23,21 +22,21 @@ export async function runMigrations() { } if (process.env.ENVIRONMENT !== "prod") { - logger.info("Skipping migrations in non-prod environment"); + console.info("Skipping migrations in non-prod environment"); return; } if (exists) { await executeScripts(); } else { - logger.info("Running migrations..."); + console.info("Running migrations..."); try { migrate(db, { migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build }); - logger.info("Migrations completed successfully."); + console.info("Migrations completed successfully."); } catch (error) { - logger.error("Error running migrations:", error); + console.error("Error running migrations:", error); } // insert process.env.APP_VERSION into the versionMigrations table @@ -61,7 +60,7 @@ async function executeScripts() { .limit(1); const startVersion = lastExecuted[0]?.version ?? "0.0.0"; - logger.info(`Starting migrations from version ${startVersion}`); + console.info(`Starting migrations from version ${startVersion}`); // Filter and sort migrations const pendingMigrations = migrations @@ -70,7 +69,7 @@ async function executeScripts() { // Run migrations in order for (const migration of pendingMigrations) { - logger.info(`Running migration ${migration.version}`); + console.info(`Running migration ${migration.version}`); try { await migration.run(); @@ -84,11 +83,11 @@ async function executeScripts() { }) .execute(); - logger.info( + console.info( `Successfully completed migration ${migration.version}` ); } catch (error) { - logger.error( + console.error( `Failed to run migration ${migration.version}:`, error ); @@ -96,9 +95,9 @@ async function executeScripts() { } } - logger.info("All migrations completed successfully"); + console.info("All migrations completed successfully"); } catch (error) { - logger.error("Migration process failed:", error); + console.error("Migration process failed:", error); throw error; } } diff --git a/server/setup/scripts/1.0.0-beta1.ts b/server/setup/scripts/1.0.0-beta1.ts index 4bc5f98e..70f2db71 100644 --- a/server/setup/scripts/1.0.0-beta1.ts +++ b/server/setup/scripts/1.0.0-beta1.ts @@ -1,7 +1,7 @@ import logger from "@server/logger"; -export default async function migration100beta1() { +export default async function migration() { logger.info("Running setup script 1.0.0-beta.1"); // SQL operations would go here in ts format logger.info("Done..."); -} \ No newline at end of file +} diff --git a/server/utils/extractBaseDomain.ts b/server/utils/extractBaseDomain.ts new file mode 100644 index 00000000..cbb12381 --- /dev/null +++ b/server/utils/extractBaseDomain.ts @@ -0,0 +1,11 @@ +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("."); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index a1d2c5bc..5d9bd111 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -398,7 +398,7 @@ export default function ResourceAuthenticationPage() { onCheckedChange={(val) => setSsoEnabled(val)} /> diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 8d3d1f21..03f04bd1 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -44,7 +44,6 @@ export default async function InvitePage(props: { await authCookieHeader() ) .catch((e) => { - console.error(e); error = formatAxiosError(e); }); @@ -68,8 +67,6 @@ export default async function InvitePage(props: { const type = cardType(); - console.log("card type is", type, error) - if (!user && type === "user_does_not_exist") { redirect(`/auth/signup?redirect=/invite?token=${params.token}`); }