♻️ validate env and add remote fossorial API as an env variable

This commit is contained in:
Fred KISSIE
2025-11-05 06:54:56 +01:00
parent 03e0e8d9c2
commit a26a441d56
7 changed files with 176 additions and 97 deletions

View File

@@ -1,14 +1,17 @@
import { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
import { pullEnv } from "./src/lib/pullEnv";
// validate env variables on build and such
pullEnv();
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
/** @type {import("next").NextConfig} */ const nextConfig: NextConfig = {
const nextConfig = {
eslint: { eslint: {
ignoreDuringBuilds: true ignoreDuringBuilds: true
}, },
output: "standalone", output: "standalone"
}; };
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);

View File

@@ -6,6 +6,7 @@ import { eq } from "drizzle-orm";
import { configSchema, readConfigFile } from "./readConfigFile"; import { configSchema, readConfigFile } from "./readConfigFile";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { build } from "@server/build"; import { build } from "@server/build";
import { pullEnv } from "@app/lib/pullEnv";
export class Config { export class Config {
private rawConfig!: z.infer<typeof configSchema>; private rawConfig!: z.infer<typeof configSchema>;
@@ -149,6 +150,7 @@ export class Config {
public async checkSupporterKey() { public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1); const [key] = await db.select().from(supporterKey).limit(1);
const env = pullEnv();
if (!key) { if (!key) {
return; return;
@@ -158,7 +160,7 @@ export class Config {
try { try {
const response = await fetch( const response = await fetch(
"https://api.fossorial.io/api/v1/license/validate", `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license/validate`,
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@@ -18,11 +18,13 @@ import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response"; import { response as sendResponse } from "@server/lib/response";
import privateConfig from "#private/lib/config"; import privateConfig from "#private/lib/config";
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
import { pullEnv } from "@app/lib/pullEnv";
async function createNewLicense(orgId: string, licenseData: any): Promise<any> { async function createNewLicense(orgId: string, licenseData: any): Promise<any> {
try { try {
const env = pullEnv();
const response = await fetch( 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", method: "PUT",
headers: { headers: {

View File

@@ -17,12 +17,17 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response"; import { response as sendResponse } from "@server/lib/response";
import privateConfig from "#private/lib/config"; 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<any> { async function fetchLicenseKeys(orgId: string): Promise<any> {
try { try {
const env = pullEnv();
const response = await fetch( 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", method: "GET",
headers: { headers: {

View File

@@ -10,6 +10,7 @@ import { supporterKey } from "@server/db";
import { db } from "@server/db"; import { db } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { pullEnv } from "@app/lib/pullEnv";
const validateSupporterKeySchema = z const validateSupporterKeySchema = z
.object({ .object({
@@ -31,6 +32,7 @@ export async function validateSupporterKey(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const env = pullEnv();
const parsedBody = validateSupporterKeySchema.safeParse(req.body); const parsedBody = validateSupporterKeySchema.safeParse(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
@@ -44,7 +46,7 @@ export async function validateSupporterKey(
const { githubUsername, key } = parsedBody.data; const { githubUsername, key } = parsedBody.data;
const response = await fetch( const response = await fetch(
"https://api.fossorial.io/api/v1/license/validate", `${env.app.fossorialRemoteAPIBaseUrl}/api/v1/license/validate`,
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@@ -1,105 +1,169 @@
import z from "zod";
import { Env } from "./types/env"; 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 { export function pullEnv(): Env {
const env = envSchema.parse(process.env);
return { return {
server: { server: {
nextPort: process.env.NEXT_PORT as string, nextPort: env.NEXT_PORT,
externalPort: process.env.SERVER_EXTERNAL_PORT as string, externalPort: env.SERVER_EXTERNAL_PORT,
sessionCookieName: process.env.SESSION_COOKIE_NAME as string, sessionCookieName: env.SESSION_COOKIE_NAME,
resourceAccessTokenParam: process.env resourceAccessTokenParam: env.RESOURCE_ACCESS_TOKEN_PARAM,
.RESOURCE_ACCESS_TOKEN_PARAM as string, resourceSessionRequestParam: env.RESOURCE_SESSION_REQUEST_PARAM,
resourceSessionRequestParam: process.env resourceAccessTokenHeadersId: env.RESOURCE_ACCESS_TOKEN_HEADERS_ID,
.RESOURCE_SESSION_REQUEST_PARAM as string, resourceAccessTokenHeadersToken:
resourceAccessTokenHeadersId: process.env env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN,
.RESOURCE_ACCESS_TOKEN_HEADERS_ID as string, reoClientId: env.REO_CLIENT_ID,
resourceAccessTokenHeadersToken: process.env maxmind_db_path: env.MAXMIND_DB_PATH
.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
}, },
app: { app: {
environment: process.env.ENVIRONMENT as string, environment: env.ENVIRONMENT,
sandbox_mode: process.env.SANDBOX_MODE === "true" ? true : false, sandbox_mode: env.SANDBOX_MODE,
version: process.env.APP_VERSION as string, version: env.APP_VERSION,
dashboardUrl: process.env.DASHBOARD_URL as string dashboardUrl: env.DASHBOARD_URL,
fossorialRemoteAPIBaseUrl: env.NEXT_PUBLIC_FOSSORIAL_REMOTE_API_URL
}, },
email: { email: {
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false emailEnabled: env.EMAIL_ENABLED
}, },
flags: { flags: {
disableUserCreateOrg: disableUserCreateOrg: env.DISABLE_USER_CREATE_ORG,
process.env.DISABLE_USER_CREATE_ORG === "true" ? true : false, disableSignupWithoutInvite: env.DISABLE_SIGNUP_WITHOUT_INVITE,
disableSignupWithoutInvite: emailVerificationRequired: env.FLAGS_EMAIL_VERIFICATION_REQUIRED,
process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true" allowRawResources: env.FLAGS_ALLOW_RAW_RESOURCES,
? true disableLocalSites: env.FLAGS_DISABLE_LOCAL_SITES,
: false, disableBasicWireguardSites: env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES,
emailVerificationRequired: enableClients: env.FLAGS_ENABLE_CLIENTS,
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true" hideSupporterKey: env.HIDE_SUPPORTER_KEY,
? true usePangolinDns: env.USE_PANGOLIN_DNS
: 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
}, },
branding: { branding: {
appName: process.env.BRANDING_APP_NAME as string, appName: env.BRANDING_APP_NAME,
background_image_path: process.env.BACKGROUND_IMAGE_PATH as string, background_image_path: env.BACKGROUND_IMAGE_PATH,
logo: { logo: {
lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string, lightPath: env.BRANDING_LOGO_LIGHT_PATH,
darkPath: process.env.BRANDING_LOGO_DARK_PATH as string, darkPath: env.BRANDING_LOGO_DARK_PATH,
authPage: { authPage: {
width: parseInt( width: env.BRANDING_LOGO_AUTH_WIDTH,
process.env.BRANDING_LOGO_AUTH_WIDTH as string height: env.BRANDING_LOGO_AUTH_HEIGHT
),
height: parseInt(
process.env.BRANDING_LOGO_AUTH_HEIGHT as string
)
}, },
navbar: { navbar: {
width: parseInt( width: env.BRANDING_LOGO_NAVBAR_WIDTH,
process.env.BRANDING_LOGO_NAVBAR_WIDTH as string height: env.BRANDING_LOGO_NAVBAR_HEIGHT
),
height: parseInt(
process.env.BRANDING_LOGO_NAVBAR_HEIGHT as string
)
} }
}, },
loginPage: { loginPage: {
titleText: process.env.LOGIN_PAGE_TITLE_TEXT as string, titleText: env.LOGIN_PAGE_TITLE_TEXT,
subtitleText: process.env.LOGIN_PAGE_SUBTITLE_TEXT as string subtitleText: env.LOGIN_PAGE_SUBTITLE_TEXT
}, },
signupPage: { signupPage: {
titleText: process.env.SIGNUP_PAGE_TITLE_TEXT as string, titleText: env.SIGNUP_PAGE_TITLE_TEXT,
subtitleText: process.env.SIGNUP_PAGE_SUBTITLE_TEXT as string subtitleText: env.SIGNUP_PAGE_SUBTITLE_TEXT
}, },
resourceAuthPage: { resourceAuthPage: {
showLogo: showLogo: env.RESOURCE_AUTH_PAGE_SHOW_LOGO,
process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO === "true" hidePoweredBy: env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY,
? true titleText: env.RESOURCE_AUTH_PAGE_TITLE_TEXT,
: false, subtitleText: env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT
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
}, },
footer: process.env.BRANDING_FOOTER as string footer: env.BRANDING_FOOTER
} }
}; };
} }

View File

@@ -4,6 +4,7 @@ export type Env = {
sandbox_mode: boolean; sandbox_mode: boolean;
version: string; version: string;
dashboardUrl: string; dashboardUrl: string;
fossorialRemoteAPIBaseUrl: string;
}; };
server: { server: {
externalPort: string; externalPort: string;
@@ -29,11 +30,11 @@ export type Env = {
enableClients: boolean; enableClients: boolean;
hideSupporterKey: boolean; hideSupporterKey: boolean;
usePangolinDns: boolean; usePangolinDns: boolean;
}, };
branding: { branding: {
appName?: string; appName?: string;
background_image_path?: string; background_image_path?: string;
logo?: { logo: {
lightPath?: string; lightPath?: string;
darkPath?: string; darkPath?: string;
authPage?: { authPage?: {
@@ -43,22 +44,22 @@ export type Env = {
navbar?: { navbar?: {
width?: number; width?: number;
height?: number; height?: number;
} };
}, };
loginPage?: { loginPage: {
titleText?: string; titleText?: string;
subtitleText?: string; subtitleText?: string;
}, };
signupPage?: { signupPage: {
titleText?: string; titleText?: string;
subtitleText?: string; subtitleText?: string;
}, };
resourceAuthPage?: { resourceAuthPage: {
showLogo?: boolean; showLogo?: boolean;
hidePoweredBy?: boolean; hidePoweredBy?: boolean;
titleText?: string; titleText?: string;
subtitleText?: string; subtitleText?: string;
}, };
footer?: string; footer?: string;
}; };
}; };