feat: initial zROC project recreation (stubs for large files pending)

- 61 files across zroc-ui/ and zroc-ova/ directories
- Full content written for: config, auth, API layers, CSS, build files,
  OVA scripts, backend routes, charts, hooks, constants
- Stubs in place for: page components, Sidebar, TopBar, docker-compose,
  authentik client, blueprint YAML, packer HCL, workflows, setup wizard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin
2026-04-12 16:20:05 -04:00
parent 74c05e5a58
commit 0500ac171c
61 changed files with 2262 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
// backend/routes/admin/users.js
'use strict';
const express = require('express');
const { authenticate, requireAdmin } = require('../../middleware/authenticate');
const authentik = require('../../authentik');
const logger = require('../../logger');
const router = express.Router();
router.use(authenticate, requireAdmin);
router.get('/', async (req, res) => {
try {
const { search = '', page = '1', pageSize = '50' } = req.query;
const result = await authentik.listUsers({
search,
page: parseInt(page, 10),
pageSize: parseInt(pageSize, 10),
});
res.json(result);
} catch (err) {
logger.error('[Users] List failed:', err.message);
res.status(502).json({ error: 'Failed to list users', detail: err.message });
}
});
router.get('/:id', async (req, res) => {
try {
const user = await authentik.getUser(req.params.id);
res.json(user);
} catch (err) {
const status = err.response?.status === 404 ? 404 : 502;
res.status(status).json({ error: 'User not found', detail: err.message });
}
});
router.post('/', async (req, res) => {
try {
const { username, name, email, isActive = true, groups = [], password } = req.body;
if (!username || !name || !email) {
return res.status(400).json({ error: 'username, name, and email are required' });
}
const user = await authentik.createUser({ username, name, email, isActive, groups, password });
logger.info(`[Users] ${req.user.username} created user ${username}`);
res.status(201).json(user);
} catch (err) {
const detail = err.response?.data || err.message;
logger.error('[Users] Create failed:', detail);
res.status(err.response?.status === 400 ? 400 : 502).json({ error: 'Failed to create user', detail });
}
});
router.patch('/:id', async (req, res) => {
try {
const { name, email, isActive, groups } = req.body;
const user = await authentik.updateUser(req.params.id, { name, email, isActive, groups });
logger.info(`[Users] ${req.user.username} updated user ${user.username}`);
res.json(user);
} catch (err) {
logger.error('[Users] Update failed:', err.message);
res.status(502).json({ error: 'Failed to update user', detail: err.message });
}
});
router.delete('/:id', async (req, res) => {
try {
const targetId = parseInt(req.params.id, 10);
if (String(targetId) === String(req.user.id) || req.user.username === 'akadmin') {
return res.status(400).json({ error: 'Cannot delete your own account or the akadmin account' });
}
await authentik.deleteUser(targetId);
logger.info(`[Users] ${req.user.username} deleted user ${targetId}`);
res.status(204).send();
} catch (err) {
logger.error('[Users] Delete failed:', err.message);
res.status(502).json({ error: 'Failed to delete user', detail: err.message });
}
});
router.post('/:id/set-password', async (req, res) => {
try {
const { password } = req.body;
if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
await authentik.setPassword(req.params.id, password);
logger.info(`[Users] ${req.user.username} reset password for user ${req.params.id}`);
res.json({ success: true });
} catch (err) {
logger.error('[Users] Password reset failed:', err.message);
res.status(502).json({ error: 'Failed to set password', detail: err.message });
}
});
router.post('/:id/setup-2fa', async (req, res) => {
try {
const { setupUrl, qrDataUrl } = await authentik.generateTwoFactorSetupLink(req.params.id);
logger.info(`[Users] ${req.user.username} generated 2FA setup link for user ${req.params.id}`);
res.json({ setupUrl, qrDataUrl });
} catch (err) {
logger.error('[Users] 2FA setup failed:', err.message);
res.status(502).json({ error: 'Failed to generate 2FA setup link', detail: err.message });
}
});
router.get('/meta/groups', async (req, res) => {
try {
const groups = await authentik.listGroups();
res.json(groups);
} catch (err) {
logger.error('[Users] Groups list failed:', err.message);
res.status(502).json({ error: 'Failed to list groups', detail: err.message });
}
});
module.exports = router;
+138
View File
@@ -0,0 +1,138 @@
// backend/routes/auth.js — OIDC login / callback / logout
'use strict';
const express = require('express');
const { Issuer, generators } = require('openid-client');
const config = require('../config');
const logger = require('../logger');
const { authenticate } = require('../middleware/authenticate');
const router = express.Router();
let oidcClient = null;
async function getOidcClient() {
if (oidcClient) return oidcClient;
const issuerUrl = `${config.authentik_url}/application/o/${config.authentik_client_id}/`;
logger.info(`[Auth] Discovering OIDC issuer at ${issuerUrl}`);
const issuer = await Issuer.discover(issuerUrl);
oidcClient = new issuer.Client({
client_id: config.authentik_client_id,
client_secret: config.authentik_client_secret,
redirect_uris: [`${config.public_url}/api/auth/callback`],
response_types: ['code'],
});
logger.info('[Auth] OIDC client initialised');
return oidcClient;
}
router.get('/login', async (req, res) => {
try {
const client = await getOidcClient();
const state = generators.state();
const nonce = generators.nonce();
const verifier = generators.codeVerifier();
const challenge = generators.codeChallenge(verifier);
req.session.oidc = { state, nonce, verifier };
const redirectTo = req.query.redirect || '/';
req.session.postLoginRedirect = redirectTo;
const authUrl = client.authorizationUrl({
scope: 'openid profile email groups',
state,
nonce,
code_challenge: challenge,
code_challenge_method: 'S256',
});
res.redirect(authUrl);
} catch (err) {
logger.error('[Auth] Login redirect failed:', err);
res.status(502).json({ error: 'Identity provider unavailable' });
}
});
router.get('/callback', async (req, res) => {
try {
const client = await getOidcClient();
const { state, nonce, verifier } = req.session.oidc || {};
if (!state) {
return res.redirect('/?error=session_expired');
}
const params = client.callbackParams(req);
const tokenSet = await client.callback(
`${config.public_url}/api/auth/callback`,
params,
{ state, nonce, code_verifier: verifier }
);
const userinfo = await client.userinfo(tokenSet.access_token);
const groups = userinfo.groups ?? [];
const role = groups.includes(config.admin_group)
? 'admin'
: groups.includes(config.viewer_group)
? 'viewer'
: 'viewer';
req.session.user = {
id: userinfo.sub,
username: userinfo.preferred_username,
name: userinfo.name,
email: userinfo.email,
role,
groups,
accessToken: tokenSet.access_token,
refreshToken: tokenSet.refresh_token,
expiresAt: tokenSet.expires_at,
};
delete req.session.oidc;
const redirect = req.session.postLoginRedirect || '/';
delete req.session.postLoginRedirect;
logger.info(`[Auth] User ${userinfo.preferred_username} (${role}) logged in`);
res.redirect(redirect);
} catch (err) {
logger.error('[Auth] Callback failed:', err);
res.redirect('/?error=auth_failed');
}
});
router.post('/logout', authenticate, async (req, res) => {
const username = req.user?.username;
const idToken = req.session.user?.accessToken;
req.session.destroy(() => {
res.clearCookie('connect.sid');
logger.info(`[Auth] User ${username} logged out`);
const endSessionUrl = `${config.authentik_url}/application/o/${config.authentik_client_id}/end-session/`;
const params = new URLSearchParams({ post_logout_redirect_uri: config.public_url });
if (idToken) params.set('id_token_hint', idToken);
res.json({ redirectUrl: `${endSessionUrl}?${params}` });
});
});
router.get('/me', authenticate, (req, res) => {
const { id, username, name, email, role, groups } = req.user;
res.json({ id, username, name, email, role, groups });
});
router.get('/status', (req, res) => {
if (req.session?.user) {
const { id, username, name, email, role } = req.session.user;
res.json({ authenticated: true, user: { id, username, name, email, role } });
} else {
res.status(401).json({ authenticated: false });
}
});
module.exports = router;
+26
View File
@@ -0,0 +1,26 @@
// backend/routes/prometheus.js
'use strict';
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const config = require('../config');
const { authenticate } = require('../middleware/authenticate');
const router = express.Router();
router.use(authenticate);
const prometheusProxy = createProxyMiddleware({
target: config.prometheus_url,
changeOrigin: true,
pathRewrite: { '^/api/prometheus': '' },
on: {
error: (err, req, res) => {
res.status(502).json({ error: 'Prometheus unreachable', detail: err.message });
},
},
});
router.use('/', prometheusProxy);
module.exports = router;