refactor: standardize env and directus logic using enhanced @mintel/next-utils
Some checks failed
Some checks failed
This commit is contained in:
@@ -62,8 +62,10 @@ services:
|
|||||||
image: directus/directus:11
|
image: directus/directus:11
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- infra
|
infra:
|
||||||
- backend
|
aliases:
|
||||||
|
- ${PROJECT_NAME:-mb-grid-solutions}-directus
|
||||||
|
backend:
|
||||||
env_file:
|
env_file:
|
||||||
- ${ENV_FILE:-.env}
|
- ${ENV_FILE:-.env}
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,43 +1,28 @@
|
|||||||
import { createDirectus, rest, authentication } from "@directus/sdk";
|
import {
|
||||||
import { config } from "./config";
|
createMintelDirectusClient,
|
||||||
|
ensureDirectusAuthenticated,
|
||||||
|
} from "@mintel/next-utils";
|
||||||
import { getServerAppServices } from "./services/create-services.server";
|
import { getServerAppServices } from "./services/create-services.server";
|
||||||
|
|
||||||
const { url, adminEmail, password, token, internalUrl } = config.directus;
|
// Initialize client using Mintel standards (environment-aware)
|
||||||
|
const client = createMintelDirectusClient();
|
||||||
// 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());
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the client is authenticated.
|
* 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() {
|
export async function ensureAuthenticated() {
|
||||||
if (token) {
|
try {
|
||||||
client.setToken(token);
|
await ensureDirectusAuthenticated(client);
|
||||||
return;
|
} catch (e) {
|
||||||
}
|
if (typeof window === "undefined") {
|
||||||
|
getServerAppServices().errors.captureException(e, {
|
||||||
if (adminEmail && password) {
|
phase: "directus_auth_standardized",
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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;
|
export default client;
|
||||||
|
|||||||
153
lib/env.ts
153
lib/env.ts
@@ -1,144 +1,33 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { validateMintelEnv, mintelEnvSchema } from "@mintel/next-utils";
|
||||||
/**
|
|
||||||
* Helper to treat empty strings as undefined.
|
|
||||||
*/
|
|
||||||
const preprocessEmptyString = (val: unknown) => (val === "" ? undefined : val);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variable schema.
|
* 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
|
export const envSchema = z.object({
|
||||||
.object({
|
...mintelEnvSchema,
|
||||||
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(),
|
|
||||||
|
|
||||||
// Analytics
|
// Project specific overrides or additions
|
||||||
UMAMI_WEBSITE_ID: z.preprocess(
|
AUTH_COOKIE_NAME: z.string().default("mb_gatekeeper_session"),
|
||||||
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"),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Error Tracking
|
INFRA_DIRECTUS_URL: z.string().url().optional(),
|
||||||
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
INFRA_DIRECTUS_TOKEN: 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>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects all environment variables from the process.
|
* Validated environment object.
|
||||||
* Explicitly references NEXT_PUBLIC_ variables for Next.js inlining.
|
*/
|
||||||
|
export const env = validateMintelEnv(envSchema.shape);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For legacy compatibility with existing code.
|
||||||
*/
|
*/
|
||||||
export function getRawEnv() {
|
export function getRawEnv() {
|
||||||
return {
|
return env;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user