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`
|
- **Host Path:** `/mnt/user/appdata/email-sigs/data`
|
||||||
- **Access Mode:** `Read/Write`
|
- **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
|
### Add Variables
|
||||||
Click **+ Add another Path, Port, Variable, Label or Device** for each of these:
|
Click **+ Add another Path, Port, Variable, Label or Device** for each of these:
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ services:
|
|||||||
- ./secrets:/app/secrets:ro
|
- ./secrets:/app/secrets:ro
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./public/assets:/app/public/assets
|
- ./public/assets:/app/public/assets
|
||||||
|
- ./public/uploads:/app/public/uploads
|
||||||
environment:
|
environment:
|
||||||
- GOOGLE_ADMIN_EMAIL
|
- GOOGLE_ADMIN_EMAIL
|
||||||
- GOOGLE_CUSTOMER_ID
|
- 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="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="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="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 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="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>
|
<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",
|
"express-basic-auth": "^1.2.1",
|
||||||
"googleapis": "^140.0.1",
|
"googleapis": "^140.0.1",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^3.0.3"
|
"node-cron": "^3.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -42,6 +42,13 @@
|
|||||||
.v-btn { padding: 4px 8px; font-size: 11px; }
|
.v-btn { padding: 4px 8px; font-size: 11px; }
|
||||||
.save-form { display: flex; gap: 8px; margin-top: 10px; }
|
.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; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -66,6 +73,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<h2>Handlebars Template (HTML)</h2>
|
||||||
<textarea id="template-editor" spellcheck="false"></textarea>
|
<textarea id="template-editor" spellcheck="false"></textarea>
|
||||||
<div class="actions">
|
<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() {
|
async function updatePreview() {
|
||||||
const templateHtml = document.getElementById('template-editor').value;
|
const templateHtml = document.getElementById('template-editor').value;
|
||||||
const userData = {
|
const userData = {
|
||||||
@@ -184,6 +243,7 @@
|
|||||||
}
|
}
|
||||||
loadTemplate();
|
loadTemplate();
|
||||||
loadVersions();
|
loadVersions();
|
||||||
|
loadImages();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const basicAuth = require('express-basic-auth');
|
const basicAuth = require('express-basic-auth');
|
||||||
const { initDb } = require('./db/sqlite');
|
const { initDb } = require('./db/sqlite');
|
||||||
const adminRoutes = require('./routes/admin');
|
const adminRoutes = require('./routes/admin');
|
||||||
@@ -11,6 +12,12 @@ const PORT = process.env.PORT || 3000;
|
|||||||
|
|
||||||
initDb();
|
initDb();
|
||||||
|
|
||||||
|
// Ensure uploads directory exists
|
||||||
|
const uploadsDir = path.join(__dirname, '../public/uploads');
|
||||||
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const auth = basicAuth({
|
const auth = basicAuth({
|
||||||
users: { [process.env.ADMIN_USERNAME || 'admin']: process.env.ADMIN_PASSWORD || 'changeme' },
|
users: { [process.env.ADMIN_USERNAME || 'admin']: process.env.ADMIN_PASSWORD || 'changeme' },
|
||||||
challenge: true,
|
challenge: true,
|
||||||
@@ -19,6 +26,7 @@ const auth = basicAuth({
|
|||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
app.use('/uploads', express.static(uploadsDir));
|
||||||
|
|
||||||
app.use('/api/admin', auth, adminRoutes);
|
app.use('/api/admin', auth, adminRoutes);
|
||||||
app.use('/api/push', auth, pushRoutes);
|
app.use('/api/push', auth, pushRoutes);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const multer = require('multer');
|
||||||
const { getRecentLogs } = require('../db/audit');
|
const { getRecentLogs } = require('../db/audit');
|
||||||
const { getAllUsers } = require('../services/googleAdmin');
|
const { getAllUsers } = require('../services/googleAdmin');
|
||||||
const {
|
const {
|
||||||
@@ -10,6 +11,16 @@ const {
|
|||||||
saveTemplate
|
saveTemplate
|
||||||
} = require('../db/templateDb');
|
} = 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) => {
|
router.get('/logs', (req, res) => {
|
||||||
const logs = getRecentLogs(parseInt(req.query.limit) || 200);
|
const logs = getRecentLogs(parseInt(req.query.limit) || 200);
|
||||||
res.json(logs);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user