add a CLIENT_CONFIG to the client, serve from BE with env vars
This commit is contained in:
1 parent
ef175950b2
commit
62031a0ab1
12 files changed
+81
-7
No files matched your search
@@ -12,6 +12,7 @@
|
||||
/>
|
||||
|
||||
<title>Checkmate</title>
|
||||
<script src="/config.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Fallback so /config.js always resolves (e.g. under the Vite dev server). In
|
||||
// the Docker image the server shadows this path and renders the real config
|
||||
// from its CLIENT_CONFIG_* environment variables.
|
||||
window.__CHECKMATE_CONFIG__ = {};
|
||||
@@ -10,8 +10,9 @@ import { useInviteForm } from "@/Hooks/useInviteForm";
|
||||
import type { InviteFormData } from "@/Validation/invite";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { runtimeConfig } from "@/Utils/runtimeConfig";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
const CLIENT_HOST = runtimeConfig.clientHost || import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
|
||||
interface InviteResponse {
|
||||
token: string;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import axios from "axios";
|
||||
import type { AxiosError } from "axios";
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { runtimeConfig } from "@/Utils/runtimeConfig";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL || "/api/v1";
|
||||
const BASE_URL =
|
||||
runtimeConfig.apiBaseUrl || import.meta.env.VITE_APP_API_BASE_URL || "/api/v1";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LogLevel } from "@/Types/Log";
|
||||
import { runtimeConfig } from "@/Utils/runtimeConfig";
|
||||
|
||||
interface LogContext {
|
||||
[key: string]: any;
|
||||
@@ -25,7 +26,8 @@ interface ErrorLogData {
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
const configuredLevel: LogLevel = import.meta.env.VITE_APP_LOG_LEVEL || "error";
|
||||
const configuredLevel: LogLevel =
|
||||
runtimeConfig.logLevel || import.meta.env.VITE_APP_LOG_LEVEL || "error";
|
||||
const configuredPriority = LOG_LEVEL_PRIORITY[configuredLevel];
|
||||
|
||||
const shouldLog = (level: LogLevel): boolean => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { LogLevel } from "@/Types/Log";
|
||||
|
||||
// Served by GET /config.js (rendered from server env) or a static config.js
|
||||
// next to index.html; loaded before the bundle in index.html.
|
||||
export interface RuntimeConfig {
|
||||
apiBaseUrl?: string;
|
||||
clientHost?: string;
|
||||
logLevel?: LogLevel;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__CHECKMATE_CONFIG__?: RuntimeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeConfig: RuntimeConfig =
|
||||
(typeof window !== "undefined" && window.__CHECKMATE_CONFIG__) || {};
|
||||
@@ -1,6 +1,9 @@
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import { runtimeConfig } from "@/Utils/runtimeConfig";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST as string | undefined;
|
||||
const CLIENT_HOST =
|
||||
runtimeConfig.clientHost ||
|
||||
(import.meta.env.VITE_APP_CLIENT_HOST as string | undefined);
|
||||
|
||||
const getClientOriginProtocol = (): string => {
|
||||
const source =
|
||||
|
||||
@@ -52,6 +52,13 @@ export const createApp = ({
|
||||
|
||||
app.use(createStatusPageDocumentCsp(allowedOrigin));
|
||||
|
||||
// Client runtime config; registered before express.static so it shadows the
|
||||
// fallback config.js in the client build output
|
||||
const clientConfigScript = `window.__CHECKMATE_CONFIG__ = ${JSON.stringify(envSettings.clientConfig).replace(/</g, "\\u003c")};`;
|
||||
app.get("/config.js", (req, res) => {
|
||||
res.set("Cache-Control", "no-cache").type("application/javascript").send(clientConfigScript);
|
||||
});
|
||||
|
||||
app.use(express.static(frontendPath));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { DbTypes, QueueModes } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { DbTypes, LogLevels, QueueModes } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { booleanCoercion } from "@/api/validation/shared.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
@@ -9,7 +9,7 @@ const envSchema = z.object({
|
||||
// Port for worker health checks
|
||||
HEALTH_PORT: z.string().default("52346"),
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
|
||||
LOG_LEVEL: z.enum(LogLevels).default("debug"),
|
||||
|
||||
// Database
|
||||
DB_CONNECTION_STRING: z.string().min(1, "Database connection string is required"),
|
||||
@@ -25,6 +25,12 @@ const envSchema = z.object({
|
||||
// Client Configuration
|
||||
CLIENT_HOST: z.string().url("CLIENT_HOST must be a valid URL"),
|
||||
|
||||
// Client runtime config overrides, rendered into GET /config.js; unset keys are
|
||||
// omitted and the client uses its same-origin defaults
|
||||
CLIENT_CONFIG_API_BASE_URL: z.string().optional(),
|
||||
CLIENT_CONFIG_CLIENT_HOST: z.string().url("CLIENT_CONFIG_CLIENT_HOST must be a valid URL").optional(),
|
||||
CLIENT_CONFIG_LOG_LEVEL: z.enum(LogLevels).optional(),
|
||||
|
||||
// Optional
|
||||
ORIGIN: z.string().optional(),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ISettingsRepository } from "@/domain/app-settings/app-settings-repository.interface.js";
|
||||
import { Settings, SettingsUpdate, type DbType, type QueueMode } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { Settings, SettingsUpdate, type ClientRuntimeConfig, type DbType, type QueueMode } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ValidatedEnv } from "@/config/envValidation.js";
|
||||
import type { StringValue } from "ms";
|
||||
@@ -16,6 +16,7 @@ export type EnvConfig = {
|
||||
queueMode: QueueMode;
|
||||
queuePrimaryProcesses: boolean;
|
||||
statusPageThemesEnabled: boolean;
|
||||
clientConfig: ClientRuntimeConfig;
|
||||
};
|
||||
|
||||
export interface ISettingsService {
|
||||
@@ -44,6 +45,11 @@ export class SettingsService implements ISettingsService {
|
||||
queueMode: env.QUEUE_MODE,
|
||||
queuePrimaryProcesses: env.QUEUE_PRIMARY_PROCESSES,
|
||||
statusPageThemesEnabled: env.STATUS_PAGE_THEMES_ENABLED,
|
||||
clientConfig: {
|
||||
...(env.CLIENT_CONFIG_API_BASE_URL && { apiBaseUrl: env.CLIENT_CONFIG_API_BASE_URL }),
|
||||
...(env.CLIENT_CONFIG_CLIENT_HOST && { clientHost: env.CLIENT_CONFIG_CLIENT_HOST }),
|
||||
...(env.CLIENT_CONFIG_LOG_LEVEL && { logLevel: env.CLIENT_CONFIG_LOG_LEVEL }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,17 @@ export type DbType = (typeof DbTypes)[number];
|
||||
export const QueueModes = ["primary", "worker"] as const;
|
||||
export type QueueMode = (typeof QueueModes)[number];
|
||||
|
||||
export const LogLevels = ["error", "warn", "info", "debug"] as const;
|
||||
export type LogLevel = (typeof LogLevels)[number];
|
||||
|
||||
// Rendered into GET /config.js as window.__CHECKMATE_CONFIG__; keys left unset
|
||||
// fall back to the client's same-origin defaults.
|
||||
export type ClientRuntimeConfig = {
|
||||
apiBaseUrl?: string;
|
||||
clientHost?: string;
|
||||
logLevel?: LogLevel;
|
||||
};
|
||||
|
||||
export interface SettingsThresholds {
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
|
||||
@@ -83,6 +83,19 @@ describe("SettingsService", () => {
|
||||
dbConnectionString: "mongodb://localhost:27017/test_db",
|
||||
dbType: "mongodb",
|
||||
statusPageThemesEnabled: false,
|
||||
clientConfig: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps only set CLIENT_CONFIG_* env vars into clientConfig", () => {
|
||||
const { service } = createService({
|
||||
CLIENT_CONFIG_API_BASE_URL: "https://api.example.com/api/v1",
|
||||
CLIENT_CONFIG_LOG_LEVEL: "warn",
|
||||
});
|
||||
|
||||
expect(service.loadSettings().clientConfig).toEqual({
|
||||
apiBaseUrl: "https://api.example.com/api/v1",
|
||||
logLevel: "warn",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user