feat: Entra ID group restriction, remove redirect URI field, user delete + email edit
This commit is contained in:
67
server.ts
67
server.ts
@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user