refactor: standardize env and directus logic using enhanced @mintel/next-utils
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 4s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
Build & Deploy / 🏗️ Build (push) Failing after 1m51s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s

This commit is contained in:
2026-02-10 23:47:18 +01:00
parent ad08c6c1f3
commit 8ff4503270
3 changed files with 41 additions and 165 deletions

View File

@@ -1,43 +1,28 @@
import { createDirectus, rest, authentication } from "@directus/sdk";
import { config } from "./config";
import {
createMintelDirectusClient,
ensureDirectusAuthenticated,
} from "@mintel/next-utils";
import { getServerAppServices } from "./services/create-services.server";
const { url, adminEmail, password, token, internalUrl } = config.directus;
// Use internal URL if on server to bypass Gatekeeper/Auth/Proxy issues
const effectiveUrl =
typeof window === "undefined" && internalUrl ? internalUrl : url;
const client = createDirectus(effectiveUrl).with(rest()).with(authentication());
// Initialize client using Mintel standards (environment-aware)
const client = createMintelDirectusClient();
/**
* Ensures the client is authenticated.
* Falls back to login with admin credentials if no static token is provided.
* Standardized using @mintel/next-utils ensureDirectusAuthenticated.
*/
export async function ensureAuthenticated() {
if (token) {
client.setToken(token);
return;
}
if (adminEmail && password) {
try {
await client.login({ email: adminEmail, password: password });
return;
} catch (e) {
if (typeof window === "undefined") {
getServerAppServices().errors.captureException(e, {
phase: "directus_auth_fallback",
});
}
console.error("Failed to authenticate with Directus login fallback:", e);
throw e;
try {
await ensureDirectusAuthenticated(client);
} catch (e) {
if (typeof window === "undefined") {
getServerAppServices().errors.captureException(e, {
phase: "directus_auth_standardized",
});
}
console.error("Failed to authenticate with Directus:", e);
throw e;
}
throw new Error(
"Missing Directus authentication credentials (token or admin email/password)",
);
}
export default client;

View File

@@ -1,144 +1,33 @@
import { z } from "zod";
/**
* Helper to treat empty strings as undefined.
*/
const preprocessEmptyString = (val: unknown) => (val === "" ? undefined : val);
import { validateMintelEnv, mintelEnvSchema } from "@mintel/next-utils";
/**
* Environment variable schema.
* Extends the default Mintel environment schema which already includes:
* - Directus (URL, TOKEN, INTERNAL_URL, etc.)
* - Mail (HOST, PORT, etc.)
* - Gotify
* - Logging
* - Analytics
*/
export const envSchema = z
.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
NEXT_PUBLIC_BASE_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
NEXT_PUBLIC_TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
export const envSchema = z.object({
...mintelEnvSchema,
// Analytics
UMAMI_WEBSITE_ID: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
UMAMI_API_ENDPOINT: z.preprocess(
preprocessEmptyString,
z.string().url().default("https://analytics.infra.mintel.me"),
),
// Project specific overrides or additions
AUTH_COOKIE_NAME: z.string().default("mb_gatekeeper_session"),
// Error Tracking
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
// Logging
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
// Mail
MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PORT: z.preprocess(
preprocessEmptyString,
z.coerce.number().default(587),
),
MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_RECIPIENTS: z.preprocess(
(val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
z.array(z.string()).default([]),
),
// Directus
DIRECTUS_URL: z.preprocess(
preprocessEmptyString,
z.string().url().default("http://localhost:8055"),
),
DIRECTUS_ADMIN_EMAIL: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
DIRECTUS_ADMIN_PASSWORD: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
DIRECTUS_API_TOKEN: z.preprocess(
preprocessEmptyString,
z.string().optional(),
),
INTERNAL_DIRECTUS_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
// Deploy Target
TARGET: z
.enum(["development", "testing", "staging", "production"])
.optional(),
// Gotify
GOTIFY_URL: z.preprocess(
preprocessEmptyString,
z.string().url().optional(),
),
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
})
.superRefine((data, ctx) => {
const target = data.NEXT_PUBLIC_TARGET || data.TARGET;
const isDev = target === "development" || !target;
const isBuildTimeValidation =
process.env.SKIP_RUNTIME_ENV_VALIDATION === "true";
const isServer = typeof window === "undefined";
// Only enforce server-only variables when running on the server.
// In the browser, non-NEXT_PUBLIC_ variables are undefined and should not trigger validation errors.
if (isServer && !isDev && !isBuildTimeValidation && !data.MAIL_HOST) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "MAIL_HOST is required in non-development environments",
path: ["MAIL_HOST"],
});
}
});
export type Env = z.infer<typeof envSchema>;
INFRA_DIRECTUS_URL: z.string().url().optional(),
INFRA_DIRECTUS_TOKEN: z.string().optional(),
});
/**
* Collects all environment variables from the process.
* Explicitly references NEXT_PUBLIC_ variables for Next.js inlining.
* Validated environment object.
*/
export const env = validateMintelEnv(envSchema.shape);
/**
* For legacy compatibility with existing code.
*/
export function getRawEnv() {
return {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
UMAMI_WEBSITE_ID:
process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
UMAMI_API_ENDPOINT:
process.env.UMAMI_API_ENDPOINT ||
process.env.UMAMI_SCRIPT_URL ||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
SENTRY_DSN: process.env.SENTRY_DSN,
LOG_LEVEL: process.env.LOG_LEVEL,
MAIL_HOST: process.env.MAIL_HOST,
MAIL_PORT: process.env.MAIL_PORT,
MAIL_USERNAME: process.env.MAIL_USERNAME,
MAIL_PASSWORD: process.env.MAIL_PASSWORD,
MAIL_FROM: process.env.MAIL_FROM,
MAIL_RECIPIENTS: process.env.MAIL_RECIPIENTS,
DIRECTUS_URL: process.env.DIRECTUS_URL,
DIRECTUS_ADMIN_EMAIL: process.env.DIRECTUS_ADMIN_EMAIL,
DIRECTUS_ADMIN_PASSWORD: process.env.DIRECTUS_ADMIN_PASSWORD,
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
TARGET: process.env.TARGET,
GOTIFY_URL: process.env.GOTIFY_URL,
GOTIFY_TOKEN: process.env.GOTIFY_TOKEN,
};
return env;
}