diff --git a/next.config.mjs b/next.config.ts similarity index 50% rename from next.config.mjs rename to next.config.ts index d771dbca..e70c4932 100644 --- a/next.config.mjs +++ b/next.config.ts @@ -1,14 +1,17 @@ +import { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; +import { pullEnv } from "./src/lib/pullEnv"; +// validate env variables on build and such +pullEnv(); + const withNextIntl = createNextIntlPlugin(); -/** @type {import("next").NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { eslint: { ignoreDuringBuilds: true }, - output: "standalone", - + output: "standalone" }; export default withNextIntl(nextConfig); diff --git a/server/lib/config.ts b/server/lib/config.ts index 6cd3413e..f71cfd51 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -6,6 +6,7 @@ import { eq } from "drizzle-orm"; import { configSchema, readConfigFile } from "./readConfigFile"; import { fromError } from "zod-validation-error"; import { build } from "@server/build"; +import { pullEnv } from "@app/lib/pullEnv"; export class Config { private rawConfig!: z.infer; @@ -149,6 +150,7 @@ export class Config { public async checkSupporterKey() { const [key] = await db.select().from(supporterKey).limit(1); + const env = pullEnv(); if (!key) { return; @@ -158,7 +160,7 @@ export class Config { try { const response = await fetch( - "https://api.fossorial.io/api/v1/license/validate", + `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license/validate`, { method: "POST", headers: { diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts index 56d65a50..dfbaaa89 100644 --- a/server/private/routers/generatedLicense/generateNewLicense.ts +++ b/server/private/routers/generatedLicense/generateNewLicense.ts @@ -18,11 +18,13 @@ import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; import privateConfig from "#private/lib/config"; import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; +import { pullEnv } from "@app/lib/pullEnv"; async function createNewLicense(orgId: string, licenseData: any): Promise { try { + const env = pullEnv(); const response = await fetch( - `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/create`, + `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license-internal/enterprise/${orgId}/create`, { method: "PUT", headers: { @@ -37,7 +39,7 @@ async function createNewLicense(orgId: string, licenseData: any): Promise { const data = await response.json(); - logger.debug("Fossorial API response:", {data}); + logger.debug("Fossorial API response:", { data }); return data; } catch (error) { console.error("Error creating new license:", error); diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts index f8da1b5a..839c8a2c 100644 --- a/server/private/routers/generatedLicense/listGeneratedLicenses.ts +++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts @@ -17,12 +17,17 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; import privateConfig from "#private/lib/config"; -import { GeneratedLicenseKey, ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types"; +import { + GeneratedLicenseKey, + ListGeneratedLicenseKeysResponse +} from "@server/routers/generatedLicense/types"; +import { pullEnv } from "@app/lib/pullEnv"; async function fetchLicenseKeys(orgId: string): Promise { try { + const env = pullEnv(); const response = await fetch( - `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/list`, + `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license-internal/enterprise/${orgId}/list`, { method: "GET", headers: { diff --git a/server/routers/supporterKey/validateSupporterKey.ts b/server/routers/supporterKey/validateSupporterKey.ts index 9d949fb5..82315017 100644 --- a/server/routers/supporterKey/validateSupporterKey.ts +++ b/server/routers/supporterKey/validateSupporterKey.ts @@ -10,6 +10,7 @@ import { supporterKey } from "@server/db"; import { db } from "@server/db"; import { eq } from "drizzle-orm"; import config from "@server/lib/config"; +import { pullEnv } from "@app/lib/pullEnv"; const validateSupporterKeySchema = z .object({ @@ -31,6 +32,7 @@ export async function validateSupporterKey( next: NextFunction ): Promise { try { + const env = pullEnv(); const parsedBody = validateSupporterKeySchema.safeParse(req.body); if (!parsedBody.success) { return next( @@ -44,7 +46,7 @@ export async function validateSupporterKey( const { githubUsername, key } = parsedBody.data; const response = await fetch( - "https://api.fossorial.io/api/v1/license/validate", + `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license/validate`, { method: "POST", headers: { diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index c55f06fe..7e5a0dc4 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -1,105 +1,169 @@ +import z from "zod"; import { Env } from "./types/env"; +const envSchema = z.object({ + // Server configuration + NEXT_PORT: z.string(), + SERVER_EXTERNAL_PORT: z.string(), + SESSION_COOKIE_NAME: z.string(), + RESOURCE_ACCESS_TOKEN_PARAM: z.string(), + RESOURCE_SESSION_REQUEST_PARAM: z.string(), + RESOURCE_ACCESS_TOKEN_HEADERS_ID: z.string(), + RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN: z.string(), + REO_CLIENT_ID: z.string().optional(), + MAXMIND_DB_PATH: z.string().optional(), + + // App configuration + ENVIRONMENT: z.string(), + SANDBOX_MODE: z + .string() + .default("false") + .transform((val) => val === "true"), + APP_VERSION: z.string(), + DASHBOARD_URL: z.string(), + NEXT_PUBLIC_FOSSORIAL_REMOTE_API_URL: z + .string() + .url() + .default("https://api.fossorial.io") + .transform((url) => url.replace(/(.*)\/?$/, "$1")), + + // Email configuration + EMAIL_ENABLED: z + .string() + .default("false") + .transform((val) => val === "true"), + + // Feature flags + DISABLE_USER_CREATE_ORG: z + .string() + .default("false") + .transform((val) => val === "true"), + DISABLE_SIGNUP_WITHOUT_INVITE: z + .string() + .default("false") + .transform((val) => val === "true"), + FLAGS_EMAIL_VERIFICATION_REQUIRED: z + .string() + .default("false") + .transform((val) => val === "true"), + FLAGS_ALLOW_RAW_RESOURCES: z + .string() + .default("false") + .transform((val) => val === "true"), + FLAGS_DISABLE_LOCAL_SITES: z + .string() + .default("false") + .transform((val) => val === "true"), + FLAGS_DISABLE_BASIC_WIREGUARD_SITES: z + .string() + .default("false") + .transform((val) => val === "true"), + FLAGS_ENABLE_CLIENTS: z + .string() + .default("false") + .transform((val) => val === "true"), + HIDE_SUPPORTER_KEY: z + .string() + .default("false") + .transform((val) => val === "true"), + USE_PANGOLIN_DNS: z + .string() + .default("false") + .transform((val) => val === "true"), + + // Branding configuration (all optional) + BRANDING_APP_NAME: z.string().optional(), + BACKGROUND_IMAGE_PATH: z.string().optional(), + BRANDING_LOGO_LIGHT_PATH: z.string().optional(), + BRANDING_LOGO_DARK_PATH: z.string().optional(), + BRANDING_LOGO_AUTH_WIDTH: z.coerce.number().optional(), + BRANDING_LOGO_AUTH_HEIGHT: z.coerce.number().optional(), + BRANDING_LOGO_NAVBAR_WIDTH: z.coerce.number().optional(), + BRANDING_LOGO_NAVBAR_HEIGHT: z.coerce.number().optional(), + LOGIN_PAGE_TITLE_TEXT: z.string().optional(), + LOGIN_PAGE_SUBTITLE_TEXT: z.string().optional(), + SIGNUP_PAGE_TITLE_TEXT: z.string().optional(), + SIGNUP_PAGE_SUBTITLE_TEXT: z.string().optional(), + RESOURCE_AUTH_PAGE_SHOW_LOGO: z + .string() + .transform((val) => val === "true") + .optional(), + RESOURCE_AUTH_PAGE_HIDE_POWERED_BY: z + .string() + .transform((val) => val === "true") + .optional(), + RESOURCE_AUTH_PAGE_TITLE_TEXT: z.string().optional(), + RESOURCE_AUTH_PAGE_SUBTITLE_TEXT: z.string().optional(), + BRANDING_FOOTER: z.string().optional() +}); + export function pullEnv(): Env { + const env = envSchema.parse(process.env); + return { server: { - nextPort: process.env.NEXT_PORT as string, - externalPort: process.env.SERVER_EXTERNAL_PORT as string, - sessionCookieName: process.env.SESSION_COOKIE_NAME as string, - resourceAccessTokenParam: process.env - .RESOURCE_ACCESS_TOKEN_PARAM as string, - resourceSessionRequestParam: process.env - .RESOURCE_SESSION_REQUEST_PARAM as string, - resourceAccessTokenHeadersId: process.env - .RESOURCE_ACCESS_TOKEN_HEADERS_ID as string, - resourceAccessTokenHeadersToken: process.env - .RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN as string, - reoClientId: process.env.REO_CLIENT_ID as string, - maxmind_db_path: process.env.MAXMIND_DB_PATH as string + nextPort: env.NEXT_PORT, + externalPort: env.SERVER_EXTERNAL_PORT, + sessionCookieName: env.SESSION_COOKIE_NAME, + resourceAccessTokenParam: env.RESOURCE_ACCESS_TOKEN_PARAM, + resourceSessionRequestParam: env.RESOURCE_SESSION_REQUEST_PARAM, + resourceAccessTokenHeadersId: env.RESOURCE_ACCESS_TOKEN_HEADERS_ID, + resourceAccessTokenHeadersToken: + env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN, + reoClientId: env.REO_CLIENT_ID, + maxmind_db_path: env.MAXMIND_DB_PATH }, app: { - environment: process.env.ENVIRONMENT as string, - sandbox_mode: process.env.SANDBOX_MODE === "true" ? true : false, - version: process.env.APP_VERSION as string, - dashboardUrl: process.env.DASHBOARD_URL as string + environment: env.ENVIRONMENT, + sandbox_mode: env.SANDBOX_MODE, + version: env.APP_VERSION, + dashboardUrl: env.DASHBOARD_URL, + fossorialRemoteAPIBaseUrl: env.NEXT_PUBLIC_FOSSORIAL_REMOTE_API_URL }, email: { - emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false + emailEnabled: env.EMAIL_ENABLED }, flags: { - disableUserCreateOrg: - process.env.DISABLE_USER_CREATE_ORG === "true" ? true : false, - disableSignupWithoutInvite: - process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true" - ? true - : false, - emailVerificationRequired: - process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true" - ? true - : false, - allowRawResources: - process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false, - disableLocalSites: - process.env.FLAGS_DISABLE_LOCAL_SITES === "true" ? true : false, - disableBasicWireguardSites: - process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES === "true" - ? true - : false, - enableClients: - process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false, - hideSupporterKey: - process.env.HIDE_SUPPORTER_KEY === "true" ? true : false, - usePangolinDns: - process.env.USE_PANGOLIN_DNS === "true" - ? true - : false + disableUserCreateOrg: env.DISABLE_USER_CREATE_ORG, + disableSignupWithoutInvite: env.DISABLE_SIGNUP_WITHOUT_INVITE, + emailVerificationRequired: env.FLAGS_EMAIL_VERIFICATION_REQUIRED, + allowRawResources: env.FLAGS_ALLOW_RAW_RESOURCES, + disableLocalSites: env.FLAGS_DISABLE_LOCAL_SITES, + disableBasicWireguardSites: env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES, + enableClients: env.FLAGS_ENABLE_CLIENTS, + hideSupporterKey: env.HIDE_SUPPORTER_KEY, + usePangolinDns: env.USE_PANGOLIN_DNS }, - branding: { - appName: process.env.BRANDING_APP_NAME as string, - background_image_path: process.env.BACKGROUND_IMAGE_PATH as string, + appName: env.BRANDING_APP_NAME, + background_image_path: env.BACKGROUND_IMAGE_PATH, logo: { - lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string, - darkPath: process.env.BRANDING_LOGO_DARK_PATH as string, + lightPath: env.BRANDING_LOGO_LIGHT_PATH, + darkPath: env.BRANDING_LOGO_DARK_PATH, authPage: { - width: parseInt( - process.env.BRANDING_LOGO_AUTH_WIDTH as string - ), - height: parseInt( - process.env.BRANDING_LOGO_AUTH_HEIGHT as string - ) + width: env.BRANDING_LOGO_AUTH_WIDTH, + height: env.BRANDING_LOGO_AUTH_HEIGHT }, navbar: { - width: parseInt( - process.env.BRANDING_LOGO_NAVBAR_WIDTH as string - ), - height: parseInt( - process.env.BRANDING_LOGO_NAVBAR_HEIGHT as string - ) + width: env.BRANDING_LOGO_NAVBAR_WIDTH, + height: env.BRANDING_LOGO_NAVBAR_HEIGHT } }, loginPage: { - titleText: process.env.LOGIN_PAGE_TITLE_TEXT as string, - subtitleText: process.env.LOGIN_PAGE_SUBTITLE_TEXT as string + titleText: env.LOGIN_PAGE_TITLE_TEXT, + subtitleText: env.LOGIN_PAGE_SUBTITLE_TEXT }, signupPage: { - titleText: process.env.SIGNUP_PAGE_TITLE_TEXT as string, - subtitleText: process.env.SIGNUP_PAGE_SUBTITLE_TEXT as string + titleText: env.SIGNUP_PAGE_TITLE_TEXT, + subtitleText: env.SIGNUP_PAGE_SUBTITLE_TEXT }, resourceAuthPage: { - showLogo: - process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO === "true" - ? true - : false, - hidePoweredBy: - process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY === "true" - ? true - : false, - titleText: process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT as string, - subtitleText: process.env - .RESOURCE_AUTH_PAGE_SUBTITLE_TEXT as string + showLogo: env.RESOURCE_AUTH_PAGE_SHOW_LOGO, + hidePoweredBy: env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY, + titleText: env.RESOURCE_AUTH_PAGE_TITLE_TEXT, + subtitleText: env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT }, - footer: process.env.BRANDING_FOOTER as string + footer: env.BRANDING_FOOTER } }; } diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index 9ded37a0..64bc8d73 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -4,6 +4,7 @@ export type Env = { sandbox_mode: boolean; version: string; dashboardUrl: string; + fossorialRemoteAPIBaseUrl: string; }; server: { externalPort: string; @@ -29,11 +30,11 @@ export type Env = { enableClients: boolean; hideSupporterKey: boolean; usePangolinDns: boolean; - }, + }; branding: { appName?: string; background_image_path?: string; - logo?: { + logo: { lightPath?: string; darkPath?: string; authPage?: { @@ -43,22 +44,22 @@ export type Env = { navbar?: { width?: number; height?: number; - } - }, - loginPage?: { + }; + }; + loginPage: { titleText?: string; subtitleText?: string; - }, - signupPage?: { + }; + signupPage: { titleText?: string; subtitleText?: string; - }, - resourceAuthPage?: { + }; + resourceAuthPage: { showLogo?: boolean; hidePoweredBy?: boolean; titleText?: string; subtitleText?: string; - }, + }; footer?: string; }; };