diff --git a/INSTALL.md b/INSTALL.md index 3713180..2d0d938 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -50,6 +50,12 @@ Click **+ Add another Path, Port, Variable, Label or Device** for each of these: - **Host Path:** `/mnt/user/appdata/email-sigs/data` - **Access Mode:** `Read/Write` +3. **Uploads Path** + - **Name:** `Uploads` + - **Container Path:** `/app/public/uploads` + - **Host Path:** `/mnt/user/appdata/email-sigs/public/uploads` + - **Access Mode:** `Read/Write` + ### Add Variables Click **+ Add another Path, Port, Variable, Label or Device** for each of these: diff --git a/docker-compose.yml b/docker-compose.yml index 69834f1..eeb980e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - ./secrets:/app/secrets:ro - ./data:/app/data - ./public/assets:/app/public/assets + - ./public/uploads:/app/public/uploads environment: - GOOGLE_ADMIN_EMAIL - GOOGLE_CUSTOMER_ID diff --git a/email-sig-manager.xml b/email-sig-manager.xml index ea00165..78ce130 100644 --- a/email-sig-manager.xml +++ b/email-sig-manager.xml @@ -25,6 +25,7 @@ /mnt/user/appdata/email-sigs/secrets /mnt/user/appdata/email-sigs/data /mnt/user/appdata/email-sigs/public/assets + /mnt/user/appdata/email-sigs/public/uploads my_customer admin diff --git a/package.json b/package.json index 5a1072f..c0b733b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "express-basic-auth": "^1.2.1", "googleapis": "^140.0.1", "handlebars": "^4.7.8", + "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3" }, "devDependencies": { diff --git a/public/editor.html b/public/editor.html index 104b5cf..7a70b6c 100644 --- a/public/editor.html +++ b/public/editor.html @@ -42,6 +42,13 @@ .v-btn { padding: 4px 8px; font-size: 11px; } .save-form { display: flex; gap: 8px; margin-top: 10px; } .save-form input { flex: 1; background: #333; border: 1px solid #444; color: #eee; padding: 8px; border-radius: 4px; font-size: 13px; } + + .asset-panel { background: #222; border: 1px solid #444; border-radius: 6px; padding: 16px; margin-top: 20px; } + .asset-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; margin-top: 10px; } + .asset-item { position: relative; border: 1px solid #333; border-radius: 4px; overflow: hidden; cursor: pointer; aspect-ratio: 1; } + .asset-item img { width: 100%; height: 100%; object-fit: cover; } + .asset-item:hover .asset-copy { opacity: 1; } + .asset-copy { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.7); color: #C9A84C; font-size: 10px; padding: 4px; text-align: center; opacity: 0; transition: opacity 0.2s; } @@ -66,6 +73,18 @@ +
+

Image Assets

+
+ + + +
+
+ +
+
+

Handlebars Template (HTML)

@@ -164,6 +183,46 @@ } } + async function loadImages() { + const images = await fetch('/api/admin/images').then(r=>r.json()).catch(()=>[]); + const list = document.getElementById('asset-list'); + list.innerHTML = images.map(img => ` +
+ +
Copy URL
+
+ `).join('') || '
No images.
'; + } + + async function uploadImage() { + const fileEl = document.getElementById('asset-upload'); + if (!fileEl.files.length) return; + + const formData = new FormData(); + formData.append('image', fileEl.files[0]); + + showStatus('Uploading...', true); + const res = await fetch('/api/admin/images', { + method: 'POST', + body: formData + }).then(r=>r.json()).catch(e=>({error:e.message})); + + if (res.ok) { + showStatus('Image uploaded!', true); + loadImages(); + } else { + showStatus('Error: '+res.error, false); + } + fileEl.value = ''; // Reset input + } + + function copyImageUrl(url) { + const fullUrl = window.location.origin + url; + navigator.clipboard.writeText(fullUrl).then(() => { + showStatus('URL copied to clipboard: ' + url, true); + }); + } + async function updatePreview() { const templateHtml = document.getElementById('template-editor').value; const userData = { @@ -184,6 +243,7 @@ } loadTemplate(); loadVersions(); + loadImages(); diff --git a/src/index.js b/src/index.js index 4db7916..db86fee 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ const express = require('express'); const path = require('path'); +const fs = require('fs'); const basicAuth = require('express-basic-auth'); const { initDb } = require('./db/sqlite'); const adminRoutes = require('./routes/admin'); @@ -11,6 +12,12 @@ const PORT = process.env.PORT || 3000; initDb(); +// Ensure uploads directory exists +const uploadsDir = path.join(__dirname, '../public/uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + const auth = basicAuth({ users: { [process.env.ADMIN_USERNAME || 'admin']: process.env.ADMIN_PASSWORD || 'changeme' }, challenge: true, @@ -19,6 +26,7 @@ const auth = basicAuth({ app.use(express.json()); app.use(express.static(path.join(__dirname, '../public'))); +app.use('/uploads', express.static(uploadsDir)); app.use('/api/admin', auth, adminRoutes); app.use('/api/push', auth, pushRoutes); diff --git a/src/routes/admin.js b/src/routes/admin.js index 009d889..db070b8 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); +const multer = require('multer'); const { getRecentLogs } = require('../db/audit'); const { getAllUsers } = require('../services/googleAdmin'); const { @@ -10,6 +11,16 @@ const { saveTemplate } = require('../db/templateDb'); +// Multer config +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, path.join(__dirname, '../../public/uploads')), + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, uniqueSuffix + '-' + file.originalname.replace(/\s+/g, '_')); + } +}); +const upload = multer({ storage }); + router.get('/logs', (req, res) => { const logs = getRecentLogs(parseInt(req.query.limit) || 200); res.json(logs); @@ -111,4 +122,23 @@ router.post('/versions/:id/deploy', (req, res) => { } }); +// Image Hosting APIs +router.post('/images', upload.single('image'), (req, res) => { + if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); + const url = `/uploads/${req.file.filename}`; + res.json({ ok: true, url }); +}); + +router.get('/images', (req, res) => { + const dir = path.join(__dirname, '../../public/uploads'); + try { + const files = fs.readdirSync(dir); + const images = files.filter(f => /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(f)) + .map(f => ({ name: f, url: `/uploads/${f}` })); + res.json(images); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + module.exports = router;