Add: Lizenzpreise, Rollen-Picker, Audit-Log Fix, Ticket Benutzerfilter

This commit is contained in:
2026-06-03 09:29:30 +02:00
parent 222b9c6fc8
commit 859ab6b28d
7 changed files with 394 additions and 9 deletions

View File

@@ -15,6 +15,8 @@ const {
getConditionalAccessPolicies,
invalidateUserSessions,
} = require('../services/graph.service');
const Database = require('better-sqlite3');
const DB_PATH = process.env.DATABASE_PATH || './database.sqlite';
/**
* GET /api/entra/users
@@ -154,7 +156,28 @@ exports.getUserLicenses = async (req, res) => {
]);
const skuMap = {};
for (const s of skus) skuMap[s.skuId] = s;
const enriched = licenses.map(l => ({ ...l, skuInfo: skuMap[l.skuId] || null }));
// Preise aus license_prices Tabelle laden
let priceRows = [];
try {
const db = new Database(DB_PATH);
priceRows = db.prepare('SELECT sku_part_number, display_name, price_per_month FROM license_prices').all();
db.close();
} catch (_) {}
const enriched = licenses.map(l => {
const skuPart = l.skuPartNumber || '';
const skuPartUpper = skuPart.toUpperCase();
// Exact match zuerst, dann partial match
const priceRow = priceRows.find(p => p.sku_part_number.toUpperCase() === skuPartUpper)
|| priceRows.find(p => skuPartUpper.includes(p.sku_part_number.toUpperCase()) || p.sku_part_number.toUpperCase().includes(skuPartUpper));
return {
...l,
skuInfo: skuMap[l.skuId] || null,
price_per_month: priceRow?.price_per_month ?? null,
price_display_name: priceRow?.display_name ?? null,
};
});
res.json({ success: true, data: enriched });
} catch (e) {
res.status(500).json({ success: false, message: e.message });

View File

@@ -509,6 +509,37 @@ async function initializeDatabase() {
)`,
`CREATE INDEX IF NOT EXISTS idx_cron_logs_job ON cron_logs(job_name, started_at)`,
`ALTER TABLE users ADD COLUMN avatar_url TEXT`,
// Lizenzpreise
`CREATE TABLE IF NOT EXISTS license_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku_part_number TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
price_per_month REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('SPE_E3', 'Microsoft 365 E3', 36.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('SPE_E5', 'Microsoft 365 E5', 57.20)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('O365_BUSINESS_PREMIUM', 'Microsoft 365 Business Premium', 22.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('O365_BUSINESS_ESSENTIALS', 'Microsoft 365 Business Basic', 6.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('O365_BUSINESS', 'Microsoft 365 Apps for Business', 8.80)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('ENTERPRISEPREMIUM', 'Office 365 E3', 23.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('ENTERPRISEPACK', 'Office 365 E3', 23.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('MCOEV', 'Microsoft Teams Phone', 8.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('FLOW_FREE', 'Power Automate Free', 0.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('POWER_BI_PRO', 'Power BI Pro', 10.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('POWER_BI_STANDARD', 'Power BI Free', 0.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('INTUNE_A', 'Microsoft Intune', 8.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('AAD_PREMIUM', 'Azure AD Premium P1', 6.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('AAD_PREMIUM_P2', 'Azure AD Premium P2', 9.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('EMS', 'Enterprise Mobility + Security E3', 8.80)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('EMSPREMIUM', 'Enterprise Mobility + Security E5', 14.80)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('VISIOCLIENT', 'Visio Plan 2', 28.10)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('PROJECTCLIENT', 'Project Plan 3', 30.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('WINDOWS_STORE', 'Windows Store', 0.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('MCOPSTN1', 'Teams Domestic Calling', 8.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('Remote_', 'Remote Desktop Services', 0.00)`,
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('SPB', 'Microsoft 365 Business Premium', 22.00)`,
];
for (const migration of migrations) {
try {

View File

@@ -0,0 +1,49 @@
const express = require('express');
const router = express.Router();
const { authenticateToken } = require('../middleware/auth');
const { requireAdmin } = require('../middleware/roleCheck');
const { asyncHandler } = require('../middleware/errorHandler');
const Database = require('better-sqlite3');
const DB_PATH = process.env.DATABASE_PATH || './database.sqlite';
router.use(authenticateToken);
// GET alle Preise
router.get('/', asyncHandler(async (req, res) => {
const db = new Database(DB_PATH);
const prices = db.prepare('SELECT * FROM license_prices ORDER BY display_name').all();
db.close();
res.json({ status: 'success', data: prices });
}));
// PUT Preis aktualisieren
router.put('/:id', requireAdmin, asyncHandler(async (req, res) => {
const { price_per_month, display_name } = req.body;
const db = new Database(DB_PATH);
db.prepare('UPDATE license_prices SET price_per_month=?, display_name=?, updated_at=CURRENT_TIMESTAMP WHERE id=?')
.run(parseFloat(price_per_month) || 0, display_name, parseInt(req.params.id));
const updated = db.prepare('SELECT * FROM license_prices WHERE id=?').get(parseInt(req.params.id));
db.close();
res.json({ status: 'success', data: updated });
}));
// POST neuer Preis
router.post('/', requireAdmin, asyncHandler(async (req, res) => {
const { sku_part_number, display_name, price_per_month } = req.body;
const db = new Database(DB_PATH);
const result = db.prepare('INSERT OR REPLACE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES (?,?,?)')
.run(sku_part_number, display_name, parseFloat(price_per_month) || 0);
const created = db.prepare('SELECT * FROM license_prices WHERE id=?').get(result.lastInsertRowid);
db.close();
res.json({ status: 'success', data: created });
}));
// DELETE
router.delete('/:id', requireAdmin, asyncHandler(async (req, res) => {
const db = new Database(DB_PATH);
db.prepare('DELETE FROM license_prices WHERE id=?').run(parseInt(req.params.id));
db.close();
res.json({ status: 'success' });
}));
module.exports = router;

View File

@@ -179,6 +179,8 @@ const feedbackRoutes = require('./routes/feedback.routes');
app.use('/api/feedback', feedbackRoutes);
const cronRoutes = require('./routes/cron.routes');
app.use('/api/cron', cronRoutes);
const licensePriceRoutes = require('./routes/licensePrice.routes');
app.use('/api/license-prices', licensePriceRoutes);
// Static file serving for uploads (PDFs)
const path = require('path');