diff --git a/app/src/app.js b/app/src/app.js index a7121cd..6d80740 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -5,7 +5,7 @@ const archiver = require('archiver'); const PDFDocument = require('pdfkit'); const QRCode = require('qrcode'); const { pool, initializeDatabase } = require('./db'); -const { normalizeInvoiceData } = require('./invoice-data'); +const { isValidSignatureDataUrl, normalizeInvoiceData } = require('./invoice-data'); const app = express(); const sessionDurationMs = 1000 * 60 * 60 * 24 * 30; @@ -362,6 +362,57 @@ app.delete('/api/payment-presets/:id', requireAuth, async (req, res, next) => { } }); +const signaturePresetResponse = (row) => ({ + id: row.id, + name: row.name, + dataUrl: row.data_url +}); + +app.get('/api/signature-presets', requireAuth, async (req, res, next) => { + try { + const result = await pool.query( + 'SELECT * FROM signature_presets WHERE user_id = $1 ORDER BY name', + [req.userId] + ); + res.json({ presets: result.rows.map(signaturePresetResponse) }); + } catch (error) { + next(error); + } +}); + +app.post('/api/signature-presets', requireAuth, async (req, res, next) => { + try { + const name = String(req.body.name || '').trim(); + const dataUrl = req.body.dataUrl; + if (!name) { + res.status(400).json({ error: 'Zadejte název podpisu.' }); + return; + } + if (!isValidSignatureDataUrl(dataUrl)) { + res.status(400).json({ error: 'Podpis musí být platný obrázek JPG nebo PNG.' }); + return; + } + const result = await pool.query(` + INSERT INTO signature_presets (user_id, name, data_url) + VALUES ($1, $2, $3) + ON CONFLICT (user_id, name) DO UPDATE SET data_url = EXCLUDED.data_url + RETURNING * + `, [req.userId, name, dataUrl]); + res.status(201).json({ preset: signaturePresetResponse(result.rows[0]) }); + } catch (error) { + next(error); + } +}); + +app.delete('/api/signature-presets/:id', requireAuth, async (req, res, next) => { + try { + await pool.query('DELETE FROM signature_presets WHERE id = $1 AND user_id = $2', [req.params.id, req.userId]); + res.status(204).end(); + } catch (error) { + next(error); + } +}); + const invoiceResponse = (row) => ({ id: row.id, invoiceNumber: row.invoice_number, diff --git a/app/src/db.js b/app/src/db.js index 4c4804a..4789f26 100644 --- a/app/src/db.js +++ b/app/src/db.js @@ -81,6 +81,15 @@ const initializeDatabase = () => { UNIQUE (user_id, name) ); + CREATE TABLE IF NOT EXISTS signature_presets ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + data_url TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, name) + ); + ALTER TABLE supplier_profiles ADD COLUMN IF NOT EXISTS nazev_spolecnosti TEXT NOT NULL DEFAULT ''; ALTER TABLE supplier_profiles ADD COLUMN IF NOT EXISTS typ_subjektu TEXT NOT NULL DEFAULT 'osoba'; ALTER TABLE clients ADD COLUMN IF NOT EXISTS nazev_spolecnosti TEXT NOT NULL DEFAULT ''; diff --git a/app/src/invoice-data.js b/app/src/invoice-data.js index 725991a..3251953 100644 --- a/app/src/invoice-data.js +++ b/app/src/invoice-data.js @@ -1,5 +1,8 @@ const SIGNATURE_DATA_URL_PATTERN = /^data:image\/(png|jpeg);base64,[A-Za-z0-9+/]+=*$/; +const isValidSignatureDataUrl = (value) => typeof value === 'string' + && SIGNATURE_DATA_URL_PATTERN.test(value); + const normalizeInvoiceData = (data) => { if (!data || !Array.isArray(data.items)) { return null; @@ -18,7 +21,7 @@ const normalizeInvoiceData = (data) => { return null; } - const signature = typeof data.signature === 'string' && SIGNATURE_DATA_URL_PATTERN.test(data.signature) + const signature = isValidSignatureDataUrl(data.signature) ? data.signature : null; @@ -32,4 +35,4 @@ const normalizeInvoiceData = (data) => { }; }; -module.exports = { normalizeInvoiceData }; +module.exports = { isValidSignatureDataUrl, normalizeInvoiceData }; diff --git a/app/src/static/index.html b/app/src/static/index.html index 7f9c63e..4c48733 100644 --- a/app/src/static/index.html +++ b/app/src/static/index.html @@ -338,11 +338,22 @@
Přetáhněte sem obrázek podpisu (420×210 px, JPG nebo PNG) nebo klikněte pro výběr souboru