feat: Implement a gatekeeper service for access control and add CMS health monitoring with a connectivity notice.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 3m8s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 3m8s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
This commit is contained in:
@@ -39,6 +39,10 @@ MAIL_RECIPIENTS=info@klz-cables.com
|
|||||||
# Logging
|
# Logging
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
GATEKEEPER_PASSWORD=klz2026
|
||||||
|
SENTRY_DSN=
|
||||||
|
# For Directus Error Tracking
|
||||||
|
# SENTRY_ENVIRONMENT is set automatically by CI
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
# Deployment Configuration (CI/CD only)
|
# Deployment Configuration (CI/CD only)
|
||||||
|
|||||||
@@ -193,6 +193,16 @@ jobs:
|
|||||||
--cache-to type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max \
|
--cache-to type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
- name: 🏗️ Gatekeeper bauen & pushen
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--pull \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
-t registry.infra.mintel.me/mintel/klz-cables-gatekeeper:$IMAGE_TAG \
|
||||||
|
--push ./gatekeeper
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 4: Deploy via SSH
|
# JOB 4: Deploy via SSH
|
||||||
@@ -227,6 +237,7 @@ jobs:
|
|||||||
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || 'directus' }}
|
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || 'directus' }}
|
||||||
DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD }}
|
DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD }}
|
||||||
DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN }}
|
DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN }}
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -265,10 +276,12 @@ jobs:
|
|||||||
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
|
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
|
||||||
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
|
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
|
||||||
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
|
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
|
||||||
|
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
|
||||||
|
|
||||||
IMAGE_TAG=$IMAGE_TAG
|
IMAGE_TAG=$IMAGE_TAG
|
||||||
TRAEFIK_HOST=$TRAEFIK_HOST
|
TRAEFIK_HOST=$TRAEFIK_HOST
|
||||||
ENV_FILE=$ENV_FILE
|
ENV_FILE=$ENV_FILE
|
||||||
|
AUTH_MIDDLEWARE=$( [[ "$TARGET" == "production" ]] && echo "compress" || echo "${PROJECT_NAME}-auth,compress" )
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
|
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Footer from '@/components/Footer';
|
|||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
|
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
|
||||||
|
import CMSConnectivityNotice from '@/components/CMSConnectivityNotice';
|
||||||
import { Metadata, Viewport } from 'next';
|
import { Metadata, Viewport } from 'next';
|
||||||
import { NextIntlClientProvider } from 'next-intl';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
import { getMessages } from 'next-intl/server';
|
import { getMessages } from 'next-intl/server';
|
||||||
@@ -20,18 +21,18 @@ export const viewport: Viewport = {
|
|||||||
viewportFit: 'cover',
|
viewportFit: 'cover',
|
||||||
themeColor: '#001a4d',
|
themeColor: '#001a4d',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({
|
||||||
children,
|
children,
|
||||||
params: {locale}
|
params: { locale }
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: {locale: string};
|
params: { locale: string };
|
||||||
}) {
|
}) {
|
||||||
// Providing all messages to the client
|
// Providing all messages to the client
|
||||||
// side is the easiest way to get started
|
// side is the easiest way to get started
|
||||||
const messages = await getMessages();
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className="scroll-smooth overflow-x-hidden">
|
<html lang={locale} className="scroll-smooth overflow-x-hidden">
|
||||||
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
||||||
@@ -42,7 +43,8 @@ export default async function LocaleLayout({
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
<CMSConnectivityNotice />
|
||||||
|
|
||||||
{/* Sends pageviews for client-side navigations */}
|
{/* Sends pageviews for client-side navigations */}
|
||||||
<AnalyticsProvider />
|
<AnalyticsProvider />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
9
app/api/health/cms/route.ts
Normal file
9
app/api/health/cms/route.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { checkHealth } from '@/lib/directus';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const health = await checkHealth();
|
||||||
|
return NextResponse.json(health, { status: health.status === 'ok' ? 200 : 503 });
|
||||||
|
}
|
||||||
72
components/CMSConnectivityNotice.tsx
Normal file
72
components/CMSConnectivityNotice.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { AlertCircle, RefreshCw, Database } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function CMSConnectivityNotice() {
|
||||||
|
const [status, setStatus] = useState<'checking' | 'ok' | 'error'>('checking');
|
||||||
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only show in development or if explicitly checking
|
||||||
|
const checkCMS = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/health/cms');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status !== 'ok') {
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMsg(data.message);
|
||||||
|
setIsVisible(true);
|
||||||
|
} else {
|
||||||
|
setStatus('ok');
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMsg('Could not connect to CMS health endpoint');
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkCMS();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 right-4 z-[9999] animate-slide-up">
|
||||||
|
<div className="bg-red-500/90 backdrop-blur-md border border-red-400 text-white p-4 rounded-2xl shadow-2xl max-w-sm">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="bg-white/20 p-2 rounded-lg">
|
||||||
|
<AlertCircle className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-bold text-sm mb-1">CMS Issue Detected</h4>
|
||||||
|
<p className="text-xs opacity-90 leading-relaxed mb-3">
|
||||||
|
{errorMsg === 'relation "products" does not exist'
|
||||||
|
? 'The database schema is missing. Please sync your local data to this environment.'
|
||||||
|
: errorMsg || 'The application cannot connect to the Directus CMS.'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="bg-white text-red-600 text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg flex items-center gap-2 hover:bg-neutral-100 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsVisible(false)}
|
||||||
|
className="bg-black/20 text-white text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg hover:bg-black/30 transition-colors"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,33 @@ services:
|
|||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
# Middlewares
|
# Middlewares
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-forward,compress"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}"
|
||||||
|
|
||||||
|
# Gatekeeper Router (to show the login page)
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=Host(${TRAEFIK_HOST}) && PathPrefix(`/gatekeeper`)"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls=true"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.service=${PROJECT_NAME:-klz-cables}-gatekeeper"
|
||||||
|
|
||||||
|
# Middleware Definitions
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME}-gatekeeper:3000/verify"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||||
|
|
||||||
|
gatekeeper:
|
||||||
|
image: registry.infra.mintel.me/mintel/klz-cables-gatekeeper:${IMAGE_TAG:-latest}
|
||||||
|
container_name: ${PROJECT_NAME:-klz-cables}-gatekeeper
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- infra
|
||||||
|
env_file:
|
||||||
|
- ${ENV_FILE:-.env}
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-gatekeeper.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
directus:
|
directus:
|
||||||
image: directus/directus:11
|
image: directus/directus:11
|
||||||
@@ -46,6 +72,9 @@ services:
|
|||||||
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
||||||
WEBSOCKETS_ENABLED: 'true'
|
WEBSOCKETS_ENABLED: 'true'
|
||||||
PUBLIC_URL: ${DIRECTUS_URL:-https://cms.klz-cables.com}
|
PUBLIC_URL: ${DIRECTUS_URL:-https://cms.klz-cables.com}
|
||||||
|
# Error Tracking
|
||||||
|
SENTRY_DSN: ${SENTRY_DSN}
|
||||||
|
SENTRY_ENVIRONMENT: ${TARGET:-development}
|
||||||
volumes:
|
volumes:
|
||||||
- ./directus/uploads:/directus/uploads
|
- ./directus/uploads:/directus/uploads
|
||||||
- ./directus/extensions:/directus/extensions
|
- ./directus/extensions:/directus/extensions
|
||||||
@@ -55,6 +84,7 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.entrypoints=websecure"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls.certresolver=le"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls.certresolver=le"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.middlewares=${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-directus.loadbalancer.server.port=8055"
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-directus.loadbalancer.server.port=8055"
|
||||||
|
|
||||||
directus-db:
|
directus-db:
|
||||||
|
|||||||
7
gatekeeper/Dockerfile
Normal file
7
gatekeeper/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "index.js"]
|
||||||
60
gatekeeper/index.js
Normal file
60
gatekeeper/index.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const GATEKEEPER_PASSWORD = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||||
|
const AUTH_COOKIE_NAME = 'klz_gatekeeper_session';
|
||||||
|
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// ForwardAuth check endpoint
|
||||||
|
app.get('/verify', (req, res) => {
|
||||||
|
const session = req.cookies[AUTH_COOKIE_NAME];
|
||||||
|
|
||||||
|
if (session === GATEKEEPER_PASSWORD) {
|
||||||
|
return res.status(200).send('OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traefik will use this to redirect if requested
|
||||||
|
const originalUrl = req.headers['x-forwarded-uri'] || '/';
|
||||||
|
const host = req.headers['x-forwarded-host'] || '';
|
||||||
|
const proto = req.headers['x-forwarded-proto'] || 'https';
|
||||||
|
|
||||||
|
// Redirect to login
|
||||||
|
res.status(401).set('X-Auth-Redirect', `${proto}://${host}/gatekeeper/login?redirect=${encodeURIComponent(originalUrl)}`).send('Unauthorized');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login page
|
||||||
|
app.get('/gatekeeper/login', (req, res) => {
|
||||||
|
res.render('login', {
|
||||||
|
error: req.query.error ? 'Invalid password' : null,
|
||||||
|
redirect: req.query.redirect || '/'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle login
|
||||||
|
app.post('/gatekeeper/login', (req, res) => {
|
||||||
|
const { password, redirect } = req.body;
|
||||||
|
|
||||||
|
if (password === GATEKEEPER_PASSWORD) {
|
||||||
|
res.cookie(AUTH_COOKIE_NAME, GATEKEEPER_PASSWORD, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
path: '/',
|
||||||
|
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||||
|
});
|
||||||
|
return res.redirect(redirect || '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(`/gatekeeper/login?error=1&redirect=${encodeURIComponent(redirect || '/')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Gatekeeper listening on port ${PORT}`);
|
||||||
|
});
|
||||||
11
gatekeeper/package.json
Normal file
11
gatekeeper/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "klz-gatekeeper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Simple branded gatekeeper for Traefik ForwardAuth",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
97
gatekeeper/views/login.ejs
Normal file
97
gatekeeper/views/login.ejs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>KLZ Cables | Access Control</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: #001a4d;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bg-grid {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
}
|
||||||
|
.accent-glow {
|
||||||
|
box-shadow: 0 0 20px rgba(130, 237, 32, 0.4);
|
||||||
|
}
|
||||||
|
.scribble-animation {
|
||||||
|
stroke-dasharray: 1000;
|
||||||
|
stroke-dashoffset: 1000;
|
||||||
|
animation: draw 2s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes draw {
|
||||||
|
to { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex items-center justify-center relative">
|
||||||
|
<!-- Background Elements -->
|
||||||
|
<div class="absolute inset-0 bg-grid pointer-events-none"></div>
|
||||||
|
<div class="absolute top-0 right-0 w-96 h-96 bg-[#82ed20]/5 blur-[120px] rounded-full -translate-y-1/2 translate-x-1/2"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-96 h-96 bg-[#82ed20]/5 blur-[120px] rounded-full translate-y-1/2 -translate-x-1/2"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 w-full max-w-md px-6">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="flex justify-center mb-12">
|
||||||
|
<svg class="h-16 w-auto" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100" height="100" rx="20" fill="#001a4d" />
|
||||||
|
<path d="M30 30L70 70" stroke="#82ed20" stroke-width="8" stroke-linecap="round" />
|
||||||
|
<path d="M70 30L30 70" stroke="#82ed20" stroke-width="8" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white/5 backdrop-blur-xl border border-white/10 p-10 rounded-[40px] shadow-2xl">
|
||||||
|
<h1 class="text-3xl font-black mb-2 tracking-tighter uppercase italic">
|
||||||
|
KLZ <span class="text-[#82ed20]">Gatekeeper</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-white/60 text-sm mb-8">This environment is strictly protected.</p>
|
||||||
|
|
||||||
|
<% if (error) { %>
|
||||||
|
<div class="bg-red-500/20 border border-red-500/50 text-red-200 p-4 rounded-2xl mb-6 text-sm flex items-center gap-3">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
<%= error %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<form action="/gatekeeper/login" method="POST" class="space-y-6">
|
||||||
|
<input type="hidden" name="redirect" value="<%= redirect %>">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black uppercase tracking-[0.2em] text-white/40 mb-2 ml-4">Access Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
class="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 focus:outline-none focus:border-[#82ed20]/50 transition-all text-lg tracking-widest text-center"
|
||||||
|
placeholder="••••••••"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-[#82ed20] text-[#001a4d] font-black uppercase tracking-[0.2em] py-5 rounded-2xl hover:bg-[#82ed20]/90 transition-all accent-glow active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
Enter Workspace →
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 text-center">
|
||||||
|
<p class="text-[10px] font-bold text-white/20 uppercase tracking-[0.4em]">
|
||||||
|
© 2026 KLZ Vertriebs GmbH
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -89,4 +89,20 @@ export async function getProductBySlug(slug: string, locale: string = 'de') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkHealth() {
|
||||||
|
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);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Unknown error',
|
||||||
|
code: error.code || 'UNKNOWN'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default client;
|
export default client;
|
||||||
|
|||||||
Reference in New Issue
Block a user