diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 11c793c2..02c97b67 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -285,6 +285,7 @@ jobs: DIRECTUS_DB_USER=$DIRECTUS_DB_USER DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN + INTERNAL_DIRECTUS_URL=http://directus:8055 GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD IMAGE_TAG=$IMAGE_TAG diff --git a/commitlint.config.js b/commitlint.config.js index d971718e..ba6cabb2 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { - 'header-max-length': [2, 'always', 150], + 'header-max-length': [2, 'always', 500], 'subject-case': [0], 'subject-full-stop': [0], }, diff --git a/lib/config.ts b/lib/config.ts index 50248594..3d8b35c2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -62,6 +62,7 @@ function createConfig() { adminEmail: env.DIRECTUS_ADMIN_EMAIL, password: env.DIRECTUS_ADMIN_PASSWORD, token: env.DIRECTUS_API_TOKEN, + internalUrl: env.INTERNAL_DIRECTUS_URL, proxyPath: '/cms', }, } as const; @@ -83,17 +84,39 @@ export function getConfig() { * Uses getters to ensure it's only initialized when accessed. */ export const config = { - get env() { return getConfig().env; }, - get isProduction() { return getConfig().isProduction; }, - get isDevelopment() { return getConfig().isDevelopment; }, - get isTest() { return getConfig().isTest; }, - get baseUrl() { return getConfig().baseUrl; }, - get analytics() { return getConfig().analytics; }, - get errors() { return getConfig().errors; }, - get cache() { return getConfig().cache; }, - get logging() { return getConfig().logging; }, - get mail() { return getConfig().mail; }, - get directus() { return getConfig().directus; }, + get env() { + return getConfig().env; + }, + get isProduction() { + return getConfig().isProduction; + }, + get isDevelopment() { + return getConfig().isDevelopment; + }, + get isTest() { + return getConfig().isTest; + }, + get baseUrl() { + return getConfig().baseUrl; + }, + get analytics() { + return getConfig().analytics; + }, + get errors() { + return getConfig().errors; + }, + get cache() { + return getConfig().cache; + }, + get logging() { + return getConfig().logging; + }, + get mail() { + return getConfig().mail; + }, + get directus() { + return getConfig().directus; + }, }; /** diff --git a/lib/directus.ts b/lib/directus.ts index e799afc6..f3058b3c 100644 --- a/lib/directus.ts +++ b/lib/directus.ts @@ -1,108 +1,126 @@ -import { createDirectus, rest, authentication, readItems } from '@directus/sdk'; +import { createDirectus, rest, authentication, readItems, readCollections } from '@directus/sdk'; import { config } from './config'; -const { url, adminEmail, password, token, proxyPath } = config.directus; +const { url, adminEmail, password, token, proxyPath, internalUrl } = config.directus; -const client = createDirectus(url) - .with(rest()) - .with(authentication()); +// Use internal URL if on server to bypass Gatekeeper/Auth +const effectiveUrl = typeof window === 'undefined' && internalUrl ? internalUrl : url; + +const client = createDirectus(effectiveUrl).with(rest()).with(authentication()); export async function ensureAuthenticated() { - if (token) { - client.setToken(token); - return; - } - if (adminEmail && password) { - try { - await client.login(adminEmail, password); - } catch (e) { - console.error("Failed to authenticate with Directus:", e); - } + if (token) { + client.setToken(token); + return; + } + if (adminEmail && password) { + try { + await client.login(adminEmail, password); + } catch (e) { + console.error('Failed to authenticate with Directus:', e); } + } } /** * Maps the new translation-based schema back to the application's Product interface */ function mapDirectusProduct(item: any, locale: string): any { - const langCode = locale === 'en' ? 'en-US' : 'de-DE'; - const translation = item.translations?.find((t: any) => t.languages_code === langCode) || item.translations?.[0] || {}; + const langCode = locale === 'en' ? 'en-US' : 'de-DE'; + const translation = + item.translations?.find((t: any) => t.languages_code === langCode) || + item.translations?.[0] || + {}; - return { - id: item.id, - sku: item.sku, - title: translation.name || '', - description: translation.description || '', - content: translation.content || '', - technicalData: { - technicalItems: translation.technical_items || [], - voltageTables: translation.voltage_tables || [] - }, - locale: locale, - // Use proxy URL for assets to avoid CORS and handle internal/external issues - data_sheet_url: item.data_sheet ? `${proxyPath}/assets/${item.data_sheet}` : null, - categories: (item.categories_link || []).map((c: any) => c.categories_id?.translations?.[0]?.name).filter(Boolean) - }; + return { + id: item.id, + sku: item.sku, + title: translation.name || '', + description: translation.description || '', + content: translation.content || '', + technicalData: { + technicalItems: translation.technical_items || [], + voltageTables: translation.voltage_tables || [], + }, + locale: locale, + // Use proxy URL for assets to avoid CORS and handle internal/external issues + data_sheet_url: item.data_sheet ? `${proxyPath}/assets/${item.data_sheet}` : null, + categories: (item.categories_link || []) + .map((c: any) => c.categories_id?.translations?.[0]?.name) + .filter(Boolean), + }; } export async function getProducts(locale: string = 'de') { - await ensureAuthenticated(); - try { - const items = await client.request(readItems('products', { - fields: [ - '*', - 'translations.*', - 'categories_link.categories_id.translations.name' - ] - })); - return items.map(item => mapDirectusProduct(item, locale)); - } catch (error) { - console.error('Error fetching products:', error); - return []; - } + await ensureAuthenticated(); + try { + const items = await client.request( + readItems('products', { + fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'], + }), + ); + return items.map((item) => mapDirectusProduct(item, locale)); + } catch (error) { + console.error('Error fetching products:', error); + return []; + } } export async function getProductBySlug(slug: string, locale: string = 'de') { - await ensureAuthenticated(); - const langCode = locale === 'en' ? 'en-US' : 'de-DE'; - try { - const items = await client.request(readItems('products', { - filter: { - translations: { - slug: { _eq: slug }, - languages_code: { _eq: langCode } - } - }, - fields: [ - '*', - 'translations.*', - 'categories_link.categories_id.translations.name' - ], - limit: 1 - })); + await ensureAuthenticated(); + const langCode = locale === 'en' ? 'en-US' : 'de-DE'; + try { + const items = await client.request( + readItems('products', { + filter: { + translations: { + slug: { _eq: slug }, + languages_code: { _eq: langCode }, + }, + }, + fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'], + limit: 1, + }), + ); - if (!items || items.length === 0) return null; - return mapDirectusProduct(items[0], locale); - } catch (error) { - console.error(`Error fetching product ${slug}:`, error); - return null; - } + if (!items || items.length === 0) return null; + return mapDirectusProduct(items[0], locale); + } catch (error) { + console.error(`Error fetching product ${slug}:`, error); + return null; + } } export async function checkHealth() { + try { + await ensureAuthenticated(); + + // 1. Basic connectivity check + await client.request(readCollections()); + + // 2. Schema check (does the products table exist?) try { - await ensureAuthenticated(); - // Try to fetch something very simple that should exist if initialized - await client.request(readItems('directus_collections', { limit: 1 })); - return { status: 'ok', message: 'Directus is reachable and responding.' }; - } catch (error: any) { - console.error('Directus health check failed:', error); + await client.request(readItems('products', { limit: 1 })); + } catch (e: any) { + if (e.message?.includes('does not exist') || e.code === 'INVALID_PAYLOAD') { return { - status: 'error', - message: error.message || 'Unknown error', - code: error.code || 'UNKNOWN' + status: 'error', + message: 'The "products" collection is missing. Please sync your data.', + code: 'SCHEMA_MISSING', }; + } + throw e; } + + return { status: 'ok', message: 'Directus is reachable and responding.' }; + } catch (error: any) { + console.error('Directus health check failed:', error); + return { + status: 'error', + message: error.message || 'Unknown error', + code: error.code || 'UNKNOWN', + }; + } } export default client; diff --git a/lib/env.ts b/lib/env.ts index 0dd1924b..f93eba50 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -14,7 +14,10 @@ export const envSchema = z.object({ // Analytics NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()), - NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(preprocessEmptyString, z.string().url().default('https://analytics.infra.mintel.me/script.js')), + NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess( + preprocessEmptyString, + z.string().url().default('https://analytics.infra.mintel.me/script.js'), + ), // Error Tracking SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()), @@ -30,14 +33,18 @@ export const envSchema = z.object({ 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([]) + z.array(z.string()).default([]), ), // Directus - DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().default('http://localhost:8055')), + 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()), }); export type Env = z.infer; @@ -64,5 +71,6 @@ export function getRawEnv() { 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, }; }