feat: Entra ID group restriction, remove redirect URI field, user delete + email edit

This commit is contained in:
Brückner
2026-06-04 13:10:56 +02:00
parent c879f84843
commit 97e1b1a665
5 changed files with 276 additions and 61 deletions

View File

@ -164,7 +164,7 @@ async function startServer() {
return res.redirect('/?auth_error=Azure+login+not+configured');
}
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
const redirectUri = `${appUrl}/api/auth/azure/callback`;
try {
const authCodeUrl = await msalClient.getAuthCodeUrl({
scopes: ['openid', 'profile', 'email'],
@ -192,7 +192,7 @@ async function startServer() {
return res.redirect('/?auth_error=Azure+login+not+configured');
}
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
const redirectUri = `${appUrl}/api/auth/azure/callback`;
try {
const result = await msalClient.acquireTokenByCode({
code: String(code),
@ -204,6 +204,13 @@ async function startServer() {
if (!email) {
return res.redirect('/?auth_error=No+email+returned+by+Microsoft');
}
const allowedGroup = getSetting('azure_allowed_group');
if (allowedGroup) {
const claims = result.idTokenClaims as { groups?: string[] } | undefined;
if (!claims?.groups?.includes(allowedGroup)) {
return res.redirect('/?auth_error=Not+a+member+of+the+required+group');
}
}
let user = db.prepare('SELECT id, name, role, email FROM users WHERE email = ?').get(email) as User | undefined;
if (!user) {
const id = uid("u");
@ -258,6 +265,41 @@ async function startServer() {
}
});
app.put('/api/users/:id', requireAuth, (req, res) => {
try {
const id = req.params.id;
const { name, email } = req.body as { name?: string; email?: string };
if (!name && !email) return res.status(400).json({ error: 'Nothing to update.' });
const existing = db.prepare('SELECT id, name, email FROM users WHERE id = ?').get(id) as User | undefined;
if (!existing) return res.status(404).json({ error: 'User not found.' });
if (email && email !== existing.email) {
const dupe = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(email, id);
if (dupe) return res.status(409).json({ error: 'Email already in use.' });
}
db.prepare('UPDATE users SET name = COALESCE(?, name), email = COALESCE(?, email) WHERE id = ?')
.run(name ?? null, email ?? null, id);
const updated = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User;
res.json(updated);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.delete('/api/users/:id', requireAuth, (req, res) => {
try {
const id = req.params.id;
if (id === req.user!.userId) return res.status(400).json({ error: 'You cannot delete your own account.' });
const existing = db.prepare('SELECT id FROM users WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ error: 'User not found.' });
const count = (db.prepare('SELECT COUNT(*) as n FROM users').get() as { n: number }).n;
if (count <= 1) return res.status(400).json({ error: 'Cannot delete the last user.' });
db.prepare('DELETE FROM users WHERE id = ?').run(id);
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// -------------------------------------------------------------
// RESTFUL API: Devices / Inventory
// -------------------------------------------------------------
@ -656,13 +698,20 @@ async function startServer() {
.all() as { id: string; hostname: string; checkMkUrl: string }[];
for (const dev of rows) {
try {
// TODO(checkmk): query the host's hard state from the CheckMK API using the
// automation secret, map 0 (UP) -> 'online' and anything else -> 'offline':
// const res = await fetch(`${CHECKMK_API_URL}/objects/host/${host}/actions/...`,
// { headers: { Authorization: `Bearer automation ${CHECKMK_API_SECRET}` } });
// const state = (await res.json()).extensions.state === 0 ? 'online' : 'offline';
// db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?')
// .run(state, new Date().toISOString(), dev.id);
const cmkHost = (() => {
try { return new URL(dev.checkMkUrl).searchParams.get('host') ?? dev.hostname; }
catch { return dev.hostname; }
})();
const res = await fetch(
`${CHECKMK_API_URL}/objects/host/${encodeURIComponent(cmkHost)}`,
{ headers: { Authorization: `Bearer automation ${CHECKMK_API_SECRET}`, Accept: 'application/json' } }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const hardState: number = data?.extensions?.state ?? -1;
const newStatus = hardState === 0 ? 'online' : 'offline';
db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?')
.run(newStatus, new Date().toISOString(), dev.id);
} catch (err) {
console.error(`[CheckMK] Status sync failed for ${dev.hostname}:`, err);
}