more cleanup
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<Config Name="Secrets Path" Target="/app/secrets" Default="/mnt/user/appdata/email-sigs/secrets" Mode="ro" Description="Path to service account JSON (sa.json)." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/email-sigs/secrets</Config>
|
||||
<Config Name="Data Path" Target="/app/data" Default="/mnt/user/appdata/email-sigs/data" Mode="rw" Description="Path for SQLite database." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/email-sigs/data</Config>
|
||||
<Config Name="Assets Path" Target="/app/public/assets" Default="/mnt/user/appdata/email-sigs/public/assets" Mode="rw" Description="Path for local assets (optional)." Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/email-sigs/public/assets</Config>
|
||||
<Config Name="Uploads Path" Target="/app/public/uploads" Default="/mnt/user/appdata/email-sigs/public/uploads" Mode="rw" Description="Path for uploaded images." Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/email-sigs/public/uploads</Config>
|
||||
<Config Name="Google Admin Email" Target="GOOGLE_ADMIN_EMAIL" Default="" Description="Workspace admin email (e.g. jason@messagepoint.tv)" Type="Variable" Display="always" Required="true" Mask="false"/>
|
||||
<Config Name="Google Customer ID" Target="GOOGLE_CUSTOMER_ID" Default="my_customer" Description="Use 'my_customer' for primary domain." Type="Variable" Display="always" Required="false" Mask="false">my_customer</Config>
|
||||
<Config Name="Admin Username" Target="ADMIN_USERNAME" Default="admin" Description="Web UI login username." Type="Variable" Display="always" Required="true" Mask="false">admin</Config>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -66,6 +73,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="asset-panel">
|
||||
<h2>Image Assets</h2>
|
||||
<div class="save-form">
|
||||
<input type="file" id="asset-upload" style="display:none" onchange="uploadImage()"/>
|
||||
<button onclick="document.getElementById('asset-upload').click()" class="v-btn">⇧ Upload Image</button>
|
||||
<button class="secondary v-btn" onclick="loadImages()">↻ Refresh</button>
|
||||
</div>
|
||||
<div class="asset-list" id="asset-list">
|
||||
<!-- Images populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Handlebars Template (HTML)</h2>
|
||||
<textarea id="template-editor" spellcheck="false"></textarea>
|
||||
<div class="actions">
|
||||
@@ -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 => `
|
||||
<div class="asset-item" onclick="copyImageUrl('${img.url}')" title="Click to copy URL">
|
||||
<img src="${img.url}" />
|
||||
<div class="asset-copy">Copy URL</div>
|
||||
</div>
|
||||
`).join('') || '<div style="color:#777; padding:10px; grid-column: 1/-1;">No images.</div>';
|
||||
}
|
||||
|
||||
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();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user