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

This commit is contained in:
2026-02-01 16:27:52 +01:00
parent 9e87720494
commit fcb3169d04
11 changed files with 327 additions and 6 deletions

7
gatekeeper/Dockerfile Normal file
View 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
View 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
View 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"
}
}

View 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 &rarr;
</button>
</form>
</div>
<div class="mt-8 text-center">
<p class="text-[10px] font-bold text-white/20 uppercase tracking-[0.4em]">
&copy; 2026 KLZ Vertriebs GmbH
</p>
</div>
</div>
</body>
</html>