From 94043baf7c8de4e7f03cea39d6147fe66439c850 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 8 Jun 2026 07:29:44 +0200 Subject: [PATCH] feat: alle lizenzen auflisten - GET /api/v1/licenses (+ ?email= filter) - Neuer admin-endpunkt GET /api/v1/licenses listet alle lizenzen inkl. email, status, max_activations, activations_used, produkt, daten. - Optionaler ?email= filter (case-insensitive) fuer eine kundenmail. - Integrationstest-abdeckung ergaenzt. Co-Authored-By: Claude Opus 4.8 --- license-backend/src/server.js | 25 +++++++++++++++++++++++++ license-backend/test/integration.mjs | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/license-backend/src/server.js b/license-backend/src/server.js index 5dd9769..07d6869 100644 --- a/license-backend/src/server.js +++ b/license-backend/src/server.js @@ -126,6 +126,23 @@ const Q = { 'INSERT INTO products (slug, name, created_at) VALUES (?, ?, ?)' ), licenseByKey: db.prepare('SELECT * FROM licenses WHERE key = ?'), + allLicenses: db.prepare(` + SELECT l.key, l.status, l.max_activations, l.email, l.note, + l.created_at, l.expires_at, p.slug AS product, + (SELECT COUNT(*) FROM activations a WHERE a.license_id = l.id) AS activations_used + FROM licenses l + JOIN products p ON p.id = l.product_id + ORDER BY l.created_at DESC + `), + licensesByEmail: db.prepare(` + SELECT l.key, l.status, l.max_activations, l.email, l.note, + l.created_at, l.expires_at, p.slug AS product, + (SELECT COUNT(*) FROM activations a WHERE a.license_id = l.id) AS activations_used + FROM licenses l + JOIN products p ON p.id = l.product_id + WHERE l.email = ? COLLATE NOCASE + ORDER BY l.created_at DESC + `), insertLicense: db.prepare(` INSERT INTO licenses (key, product_id, max_activations, status, email, note, created_at, expires_at) VALUES (@key, @product_id, @max_activations, 'active', @email, @note, @created_at, @expires_at) @@ -657,6 +674,14 @@ app.post('/api/v1/licenses', adminOnly, (req, res) => { }); }); +// List all licenses (each row includes its email). Optional ?email=… filters to +// one customer's licenses (case-insensitive exact match). +app.get('/api/v1/licenses', adminOnly, (req, res) => { + const email = req.query.email ? String(req.query.email).trim() : ''; + const rows = email ? Q.licensesByEmail.all(email) : Q.allLicenses.all(); + return res.json({ ok: true, count: rows.length, licenses: rows }); +}); + // Inspect a license (status + bound domains). app.get('/api/v1/licenses/:key', adminOnly, (req, res) => { const license = Q.licenseByKey.get(req.params.key); diff --git a/license-backend/test/integration.mjs b/license-backend/test/integration.mjs index 47275d0..155e1e1 100644 --- a/license-backend/test/integration.mjs +++ b/license-backend/test/integration.mjs @@ -142,6 +142,18 @@ try { const info = await admin('GET', `/api/v1/licenses/${key1}`); ok('inspect shows 1 domain (kunde-b)', info.json.activations_used === 1 && info.json.domains[0].domain === 'kunde-b.de', JSON.stringify(info.json)); + // List all licenses (each row carries its email) + const listAll = await admin('GET', '/api/v1/licenses'); + ok('list licenses ok', listAll.status === 200 && listAll.json.ok && Array.isArray(listAll.json.licenses), JSON.stringify(listAll.json)); + ok('list includes key1 with email', listAll.json.licenses.some((l) => l.key === key1 && l.email === 'a@b.de'), JSON.stringify(listAll.json)); + ok('list rows have activations_used', listAll.json.licenses.every((l) => typeof l.activations_used === 'number')); + + // Filter by email (case-insensitive) + const byMail = await admin('GET', '/api/v1/licenses?email=A@B.de'); + ok('filter by email finds key1', byMail.json.ok && byMail.json.licenses.length === 1 && byMail.json.licenses[0].key === key1, JSON.stringify(byMail.json)); + const byMailNone = await admin('GET', '/api/v1/licenses?email=nobody@x.de'); + ok('filter by unknown email → empty', byMailNone.json.ok && byMailNone.json.count === 0, JSON.stringify(byMailNone.json)); + // ── Scan endpoint guards ── // Unactivated domain → 403 const scanUnbound = await pub('/api/v1/scan', { key: key1, product: P, domain: 'kunde-a.de' });