wiki-plugin-shoppe 0.0.23 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/scripts/shoppe-sign.js +71 -1
- package/server/server.js +179 -26
package/package.json
CHANGED
|
@@ -185,7 +185,7 @@ function createZip() {
|
|
|
185
185
|
execSync(`powershell -NoProfile -Command "${psCmd}"`, { stdio: 'pipe' });
|
|
186
186
|
} else {
|
|
187
187
|
execSync(
|
|
188
|
-
`zip -r "${outputZip}" . -x "*/shoppe-key.json"`,
|
|
188
|
+
`zip -r "${outputZip}" . -x "*/shoppe-key.json" -x "*.mp4" -x "*.mov" -x "*.mkv" -x "*.webm" -x "*.avi"`,
|
|
189
189
|
{ cwd: SHOPPE_DIR, stdio: 'pipe' }
|
|
190
190
|
);
|
|
191
191
|
}
|
|
@@ -273,6 +273,71 @@ async function orders() {
|
|
|
273
273
|
console.log('');
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
// ── upload — generate a signed shoppe URL for video uploading ────────────────
|
|
277
|
+
|
|
278
|
+
async function upload() {
|
|
279
|
+
let sessionless;
|
|
280
|
+
try {
|
|
281
|
+
sessionless = require('sessionless-node');
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error('❌ sessionless-node is not installed.');
|
|
284
|
+
console.error(' Run: npm install');
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const manifest = readManifest();
|
|
289
|
+
|
|
290
|
+
if (!manifest.uuid) {
|
|
291
|
+
console.error('❌ manifest.json is missing uuid.');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (fs.existsSync(LOCAL_KEY)) {
|
|
296
|
+
console.error('⚠️ shoppe-key.json is still in this folder.');
|
|
297
|
+
console.error(' Run node shoppe-sign.js init first.');
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const keyData = loadStoredKey(manifest.uuid);
|
|
302
|
+
|
|
303
|
+
const timestamp = Date.now().toString();
|
|
304
|
+
const message = timestamp + manifest.uuid;
|
|
305
|
+
|
|
306
|
+
sessionless.getKeys = () => ({ pubKey: keyData.pubKey, privateKey: keyData.privateKey });
|
|
307
|
+
const signature = await sessionless.sign(message);
|
|
308
|
+
|
|
309
|
+
const wikiUrlArg = process.argv[3];
|
|
310
|
+
const baseUrl = wikiUrlArg
|
|
311
|
+
? wikiUrlArg.replace(/\/+$/, '')
|
|
312
|
+
: manifest.wikiUrl
|
|
313
|
+
? manifest.wikiUrl.replace(/\/plugin.*$/, '')
|
|
314
|
+
: null;
|
|
315
|
+
|
|
316
|
+
const shoppePath = `/plugin/shoppe/${manifest.uuid}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`;
|
|
317
|
+
const fullUrl = baseUrl ? `${baseUrl}${shoppePath}` : null;
|
|
318
|
+
|
|
319
|
+
console.log('\n🎬 Signed shoppe URL for video uploading (valid for 24 hours):\n');
|
|
320
|
+
if (fullUrl) {
|
|
321
|
+
console.log(' ' + fullUrl);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(' Path: ' + shoppePath);
|
|
324
|
+
console.log('\n Prepend your wiki URL, e.g.:');
|
|
325
|
+
console.log(' https://mywiki.com' + shoppePath);
|
|
326
|
+
console.log('\n Or pass your wiki URL as an argument:');
|
|
327
|
+
console.log(' node shoppe-sign.js upload https://mywiki.com');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (fullUrl) {
|
|
331
|
+
console.log('\n Opening in browser...');
|
|
332
|
+
try {
|
|
333
|
+
const open = process.platform === 'win32' ? 'start' :
|
|
334
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
335
|
+
execSync(`${open} "${fullUrl}"`, { stdio: 'ignore' });
|
|
336
|
+
} catch (_) {}
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
|
|
276
341
|
// ── payouts — open Stripe Connect Express onboarding ─────────────────────────
|
|
277
342
|
|
|
278
343
|
async function payouts() {
|
|
@@ -347,6 +412,11 @@ async function payouts() {
|
|
|
347
412
|
const command = process.argv[2];
|
|
348
413
|
if (command === 'init') {
|
|
349
414
|
init();
|
|
415
|
+
} else if (command === 'upload') {
|
|
416
|
+
upload().catch(err => {
|
|
417
|
+
console.error('❌ ', err.message);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
});
|
|
350
420
|
} else if (command === 'orders') {
|
|
351
421
|
orders().catch(err => {
|
|
352
422
|
console.error('❌ ', err.message);
|
package/server/server.js
CHANGED
|
@@ -261,6 +261,13 @@ function generateBundleBuffer(tenant, ownerPrivateKey, ownerPubKey, wikiOrigin)
|
|
|
261
261
|
'Add or update content, then run `node shoppe-sign.js` again.',
|
|
262
262
|
'Each upload overwrites existing items and adds new ones.',
|
|
263
263
|
'',
|
|
264
|
+
'## Uploading videos',
|
|
265
|
+
'',
|
|
266
|
+
'Run: `node shoppe-sign.js upload`',
|
|
267
|
+
'',
|
|
268
|
+
'Opens your shoppe page with a signed URL (valid for 24 hours).',
|
|
269
|
+
'Any video items without a file will show an "Upload Video" button.',
|
|
270
|
+
'',
|
|
264
271
|
'## Viewing orders',
|
|
265
272
|
'',
|
|
266
273
|
'Run: `node shoppe-sign.js orders`',
|
|
@@ -560,6 +567,20 @@ async function sanoraUploadImage(tenant, title, imageBuffer, filename) {
|
|
|
560
567
|
|
|
561
568
|
// ============================================================
|
|
562
569
|
// LUCILLE HELPERS
|
|
570
|
+
async function sanoraDeleteProduct(tenant, title) {
|
|
571
|
+
const { uuid, keys } = tenant;
|
|
572
|
+
const timestamp = Date.now().toString();
|
|
573
|
+
const message = timestamp + uuid + title;
|
|
574
|
+
|
|
575
|
+
sessionless.getKeys = () => keys;
|
|
576
|
+
const signature = await sessionless.sign(message);
|
|
577
|
+
|
|
578
|
+
await fetch(
|
|
579
|
+
`${getSanoraUrl()}/user/${uuid}/product/${encodeURIComponent(title)}?timestamp=${timestamp}&signature=${encodeURIComponent(signature)}`,
|
|
580
|
+
{ method: 'DELETE' }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
563
584
|
// ============================================================
|
|
564
585
|
|
|
565
586
|
async function lucilleCreateUser(lucilleUrl) {
|
|
@@ -1125,26 +1146,10 @@ async function processArchive(zipPath) {
|
|
|
1125
1146
|
await sanoraUploadImage(tenant, title, coverBuf, coverFile);
|
|
1126
1147
|
}
|
|
1127
1148
|
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const videoBuf = fs.readFileSync(path.join(entryPath, videoFilename));
|
|
1133
|
-
const localHash = crypto.createHash('sha256').update(videoBuf).digest('hex');
|
|
1134
|
-
|
|
1135
|
-
const existing = existingLucilleVideos[title];
|
|
1136
|
-
if (existing && existing.contentHash && existing.contentHash === localHash) {
|
|
1137
|
-
console.log(`[shoppe] ⏩ video unchanged, skipping upload: ${title}`);
|
|
1138
|
-
results.videos.push({ title, price, skipped: true });
|
|
1139
|
-
} else {
|
|
1140
|
-
await lucilleRegisterVideo(tenant, title, description, tags, effectiveLucilleUrl);
|
|
1141
|
-
await lucilleUploadVideo(tenant, title, videoBuf, videoFilename, effectiveLucilleUrl);
|
|
1142
|
-
results.videos.push({ title, price });
|
|
1143
|
-
console.log(`[shoppe] 🎬 video: ${title}`);
|
|
1144
|
-
}
|
|
1145
|
-
} else {
|
|
1146
|
-
results.warnings.push(`video "${title}": no video file found (expected .mp4/.mov/.mkv/.webm/.avi)`);
|
|
1147
|
-
}
|
|
1149
|
+
// Register video metadata in Lucille (file upload happens separately via upload-info endpoint)
|
|
1150
|
+
await lucilleRegisterVideo(tenant, title, description, tags, effectiveLucilleUrl);
|
|
1151
|
+
results.videos.push({ title, price });
|
|
1152
|
+
console.log(`[shoppe] 🎬 video registered: ${title} (upload file separately)`);
|
|
1148
1153
|
} catch (err) {
|
|
1149
1154
|
console.warn(`[shoppe] ⚠️ video ${entry}: ${err.message}`);
|
|
1150
1155
|
results.warnings.push(`video "${entry}": ${err.message}`);
|
|
@@ -1262,7 +1267,8 @@ async function getShoppeGoods(tenant) {
|
|
|
1262
1267
|
shipping: product.shipping || 0,
|
|
1263
1268
|
image: product.image ? `${getSanoraUrl()}/images/${product.image}` : null,
|
|
1264
1269
|
url: (bucketName && redirects[bucketName]) || defaultUrl,
|
|
1265
|
-
...(lucillePlayerUrl && { lucillePlayerUrl })
|
|
1270
|
+
...(lucillePlayerUrl && { lucillePlayerUrl }),
|
|
1271
|
+
...(product.category === 'video' && { shoppeId: tenant.uuid })
|
|
1266
1272
|
};
|
|
1267
1273
|
const bucket = goods[bucketName];
|
|
1268
1274
|
if (bucket) bucket.push(item);
|
|
@@ -1398,12 +1404,12 @@ const CATEGORY_EMOJI = { book: '📚', music: '🎵', post: '📝', album: '🖼
|
|
|
1398
1404
|
// Validate an owner-signed request (used for browser-facing owner routes).
|
|
1399
1405
|
// Expects req.query.timestamp and req.query.signature.
|
|
1400
1406
|
// Returns an error string if invalid, null if valid.
|
|
1401
|
-
function checkOwnerSignature(req, tenant) {
|
|
1407
|
+
function checkOwnerSignature(req, tenant, maxAgeMs = 5 * 60 * 1000) {
|
|
1402
1408
|
if (!tenant.ownerPubKey) return 'This shoppe was registered before owner signing was added';
|
|
1403
1409
|
const { timestamp, signature } = req.query;
|
|
1404
1410
|
if (!timestamp || !signature) return 'Missing timestamp or signature — generate a fresh URL with: node shoppe-sign.js orders';
|
|
1405
1411
|
const age = Date.now() - parseInt(timestamp, 10);
|
|
1406
|
-
if (isNaN(age) || age < 0 || age >
|
|
1412
|
+
if (isNaN(age) || age < 0 || age > maxAgeMs) return 'URL has expired — generate a new one with: node shoppe-sign.js orders';
|
|
1407
1413
|
const message = timestamp + tenant.uuid;
|
|
1408
1414
|
if (!sessionless.verifySignature(signature, message, tenant.ownerPubKey)) return 'Signature invalid';
|
|
1409
1415
|
return null;
|
|
@@ -1539,12 +1545,35 @@ function renderCards(items, category) {
|
|
|
1539
1545
|
}
|
|
1540
1546
|
return items.map(item => {
|
|
1541
1547
|
const isVideo = !!item.lucillePlayerUrl;
|
|
1548
|
+
const isUnuploadedVideo = item.shoppeId && !item.lucillePlayerUrl;
|
|
1542
1549
|
const imgHtml = item.image
|
|
1543
1550
|
? `<div class="card-img${isVideo ? ' card-video-play' : ''}"><img src="${item.image}" alt="" loading="lazy"></div>`
|
|
1544
|
-
:
|
|
1551
|
+
: isUnuploadedVideo
|
|
1552
|
+
? `<div class="card-img-placeholder card-video-upload"><span style="font-size:44px">🎬</span></div>`
|
|
1553
|
+
: `<div class="card-img-placeholder">${CATEGORY_EMOJI[category] || '🎁'}</div>`;
|
|
1545
1554
|
const priceHtml = (item.price > 0 || category === 'product')
|
|
1546
1555
|
? `<div class="price">$${(item.price / 100).toFixed(2)}${item.shipping ? ` <span class="shipping">+ $${(item.shipping / 100).toFixed(2)} shipping</span>` : ''}</div>`
|
|
1547
1556
|
: '';
|
|
1557
|
+
if (isUnuploadedVideo) {
|
|
1558
|
+
const safeTitle = item.title.replace(/'/g, "\\'");
|
|
1559
|
+
return `
|
|
1560
|
+
<div class="card" id="video-card-${item.shoppeId}-${item.title.replace(/[^a-z0-9]/gi,'_')}">
|
|
1561
|
+
${imgHtml}
|
|
1562
|
+
<div class="card-body">
|
|
1563
|
+
<div class="card-title">${item.title}</div>
|
|
1564
|
+
${item.description ? `<div class="card-desc">${item.description}</div>` : ''}
|
|
1565
|
+
${priceHtml}
|
|
1566
|
+
<div class="video-upload-area" id="upload-area-${item.shoppeId}-${item.title.replace(/[^a-z0-9]/gi,'_')}">
|
|
1567
|
+
<label class="upload-btn-label">
|
|
1568
|
+
📁 Upload Video
|
|
1569
|
+
<input type="file" accept="video/*" style="display:none"
|
|
1570
|
+
onchange="startVideoUpload(this,'${item.shoppeId}','${safeTitle}')">
|
|
1571
|
+
</label>
|
|
1572
|
+
<div class="upload-progress" style="display:none"></div>
|
|
1573
|
+
</div>
|
|
1574
|
+
</div>
|
|
1575
|
+
</div>`;
|
|
1576
|
+
}
|
|
1548
1577
|
const clickHandler = isVideo
|
|
1549
1578
|
? `playVideo('${item.lucillePlayerUrl}')`
|
|
1550
1579
|
: `window.open('${item.url}','_blank')`;
|
|
@@ -1560,7 +1589,7 @@ function renderCards(items, category) {
|
|
|
1560
1589
|
}).join('');
|
|
1561
1590
|
}
|
|
1562
1591
|
|
|
1563
|
-
function generateShoppeHTML(tenant, goods) {
|
|
1592
|
+
function generateShoppeHTML(tenant, goods, uploadAuth = null) {
|
|
1564
1593
|
const total = Object.values(goods).flat().length;
|
|
1565
1594
|
const tabs = [
|
|
1566
1595
|
{ id: 'all', label: 'All', count: total, always: true },
|
|
@@ -1622,6 +1651,12 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1622
1651
|
.video-modal-content iframe { width: 100%; height: 100%; border: none; display: block; }
|
|
1623
1652
|
.video-modal-close { position: absolute; top: 10px; right: 12px; z-index: 2; background: rgba(0,0,0,0.5); border: none; color: #fff; font-size: 20px; line-height: 1; padding: 4px 10px; border-radius: 6px; cursor: pointer; }
|
|
1624
1653
|
.video-modal-close:hover { background: rgba(0,0,0,0.8); }
|
|
1654
|
+
.card-video-upload { cursor: default !important; }
|
|
1655
|
+
.upload-btn-label { display: inline-block; background: #0066cc; color: white; border-radius: 8px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; margin-top: 8px; }
|
|
1656
|
+
.upload-btn-label:hover { background: #0052a3; }
|
|
1657
|
+
.upload-progress { margin-top: 8px; font-size: 12px; color: #555; }
|
|
1658
|
+
.upload-progress-bar { height: 4px; background: #e0e0e0; border-radius: 2px; margin-top: 4px; overflow: hidden; }
|
|
1659
|
+
.upload-progress-bar-fill { height: 100%; background: #0066cc; border-radius: 2px; transition: width 0.2s; }
|
|
1625
1660
|
</style>
|
|
1626
1661
|
</head>
|
|
1627
1662
|
<body>
|
|
@@ -1653,6 +1688,7 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1653
1688
|
</div>
|
|
1654
1689
|
</div>
|
|
1655
1690
|
<script>
|
|
1691
|
+
const UPLOAD_AUTH = ${uploadAuth ? JSON.stringify(uploadAuth) : 'null'};
|
|
1656
1692
|
function show(id, tab) {
|
|
1657
1693
|
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
|
1658
1694
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
@@ -1668,6 +1704,58 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1668
1704
|
document.getElementById('video-iframe').src = '';
|
|
1669
1705
|
}
|
|
1670
1706
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeVideo(); });
|
|
1707
|
+
|
|
1708
|
+
async function startVideoUpload(input, shoppeId, title) {
|
|
1709
|
+
const file = input.files[0];
|
|
1710
|
+
if (!file) return;
|
|
1711
|
+
|
|
1712
|
+
const areaId = 'upload-area-' + shoppeId + '-' + title.replace(/[^a-z0-9]/gi,'_');
|
|
1713
|
+
const area = document.getElementById(areaId);
|
|
1714
|
+
const progressDiv = area.querySelector('.upload-progress');
|
|
1715
|
+
const label = area.querySelector('.upload-btn-label');
|
|
1716
|
+
|
|
1717
|
+
label.style.display = 'none';
|
|
1718
|
+
progressDiv.style.display = 'block';
|
|
1719
|
+
progressDiv.innerHTML = 'Getting upload credentials…';
|
|
1720
|
+
|
|
1721
|
+
try {
|
|
1722
|
+
if (!UPLOAD_AUTH) throw new Error('Not authorized to upload — visit the shoppe via a signed URL (node shoppe-sign.js upload)');
|
|
1723
|
+
const authParams = '?timestamp=' + encodeURIComponent(UPLOAD_AUTH.timestamp) + '&signature=' + encodeURIComponent(UPLOAD_AUTH.signature);
|
|
1724
|
+
const infoRes = await fetch('/plugin/shoppe/' + shoppeId + '/video/' + encodeURIComponent(title) + '/upload-info' + authParams);
|
|
1725
|
+
if (!infoRes.ok) throw new Error('Could not get upload credentials (' + infoRes.status + ')');
|
|
1726
|
+
const { uploadUrl, timestamp, signature } = await infoRes.json();
|
|
1727
|
+
|
|
1728
|
+
progressDiv.innerHTML = 'Uploading… 0%<div class="upload-progress-bar"><div class="upload-progress-bar-fill" id="fill-' + areaId + '" style="width:0%"></div></div>';
|
|
1729
|
+
|
|
1730
|
+
const form = new FormData();
|
|
1731
|
+
form.append('video', file, file.name);
|
|
1732
|
+
|
|
1733
|
+
await new Promise((resolve, reject) => {
|
|
1734
|
+
const xhr = new XMLHttpRequest();
|
|
1735
|
+
xhr.upload.onprogress = e => {
|
|
1736
|
+
if (e.lengthComputable) {
|
|
1737
|
+
const pct = Math.round(e.loaded / e.total * 100);
|
|
1738
|
+
progressDiv.querySelector('div').textContent = '';
|
|
1739
|
+
progressDiv.firstChild.textContent = 'Uploading… ' + pct + '%';
|
|
1740
|
+
const fill = document.getElementById('fill-' + areaId);
|
|
1741
|
+
if (fill) fill.style.width = pct + '%';
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
xhr.onload = () => xhr.status === 200 ? resolve() : reject(new Error('Upload failed: ' + xhr.status));
|
|
1745
|
+
xhr.onerror = () => reject(new Error('Network error during upload'));
|
|
1746
|
+
xhr.open('PUT', uploadUrl);
|
|
1747
|
+
xhr.setRequestHeader('x-pn-timestamp', timestamp);
|
|
1748
|
+
xhr.setRequestHeader('x-pn-signature', signature);
|
|
1749
|
+
xhr.send(form);
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
progressDiv.innerHTML = '✅ Uploaded! Reloading…';
|
|
1753
|
+
setTimeout(() => location.reload(), 1500);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
progressDiv.innerHTML = '❌ ' + err.message;
|
|
1756
|
+
label.style.display = 'inline-block';
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1671
1759
|
</script>
|
|
1672
1760
|
</body>
|
|
1673
1761
|
</html>`;
|
|
@@ -1800,6 +1888,36 @@ async function startServer(params) {
|
|
|
1800
1888
|
res.json({ success: true, tenants: safe });
|
|
1801
1889
|
});
|
|
1802
1890
|
|
|
1891
|
+
// Delete a shoppe tenant (owner only)
|
|
1892
|
+
app.delete('/plugin/shoppe/:identifier', owner, async (req, res) => {
|
|
1893
|
+
try {
|
|
1894
|
+
const tenant = getTenantByIdentifier(req.params.identifier);
|
|
1895
|
+
if (!tenant) return res.status(404).json({ error: 'tenant not found' });
|
|
1896
|
+
|
|
1897
|
+
// Fetch all products from Sanora and fire-and-forget delete each one
|
|
1898
|
+
const sanoraUrl = getSanoraUrl();
|
|
1899
|
+
fetch(`${sanoraUrl}/products/${tenant.uuid}`)
|
|
1900
|
+
.then(r => r.json())
|
|
1901
|
+
.then(products => {
|
|
1902
|
+
for (const title of Object.keys(products)) {
|
|
1903
|
+
sanoraDeleteProduct(tenant, title).catch(err =>
|
|
1904
|
+
console.warn(`[shoppe] delete product "${title}" failed:`, err.message)
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
})
|
|
1908
|
+
.catch(err => console.warn('[shoppe] fetch products for delete failed:', err.message));
|
|
1909
|
+
|
|
1910
|
+
// Remove tenant from local registry
|
|
1911
|
+
const tenants = loadTenants();
|
|
1912
|
+
delete tenants[tenant.uuid];
|
|
1913
|
+
saveTenants(tenants);
|
|
1914
|
+
|
|
1915
|
+
res.json({ success: true, deleted: tenant.uuid });
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
res.status(404).json({ error: err.message });
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1803
1921
|
// Public directory — name, emojicode, and shoppe URL only
|
|
1804
1922
|
app.get('/plugin/shoppe/directory', (req, res) => {
|
|
1805
1923
|
const tenants = loadTenants();
|
|
@@ -2473,6 +2591,35 @@ async function startServer(params) {
|
|
|
2473
2591
|
}
|
|
2474
2592
|
});
|
|
2475
2593
|
|
|
2594
|
+
// GET /plugin/shoppe/:id/video/:title/upload-info
|
|
2595
|
+
// Returns a pre-signed lucille upload URL so the browser can PUT the video file directly to lucille.
|
|
2596
|
+
// Auth: shoppe tenant owner signature (timestamp + uuid), valid for 24 hours.
|
|
2597
|
+
// Generate the signed URL with: node shoppe-sign.js upload
|
|
2598
|
+
app.get('/plugin/shoppe/:identifier/video/:title/upload-info', async (req, res) => {
|
|
2599
|
+
try {
|
|
2600
|
+
const tenant = getTenantByIdentifier(req.params.identifier);
|
|
2601
|
+
if (!tenant) return res.status(404).json({ error: 'tenant not found' });
|
|
2602
|
+
|
|
2603
|
+
const sigErr = checkOwnerSignature(req, tenant, 24 * 60 * 60 * 1000);
|
|
2604
|
+
if (sigErr) return res.status(403).json({ error: sigErr });
|
|
2605
|
+
|
|
2606
|
+
if (!tenant.lucilleKeys) return res.status(400).json({ error: 'tenant has no lucille user — re-register' });
|
|
2607
|
+
|
|
2608
|
+
const title = req.params.title;
|
|
2609
|
+
const lucilleBase = getLucilleUrl().replace(/\/$/, '');
|
|
2610
|
+
const { uuid: lucilleUuid, pubKey, privateKey } = tenant.lucilleKeys;
|
|
2611
|
+
|
|
2612
|
+
const timestamp = Date.now().toString();
|
|
2613
|
+
sessionless.getKeys = () => ({ pubKey, privateKey });
|
|
2614
|
+
const signature = await sessionless.sign(timestamp + pubKey);
|
|
2615
|
+
|
|
2616
|
+
const uploadUrl = `${lucilleBase}/user/${lucilleUuid}/video/${encodeURIComponent(title)}/file`;
|
|
2617
|
+
res.json({ uploadUrl, timestamp, signature });
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
res.status(500).json({ error: err.message });
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2476
2623
|
// Goods JSON (public)
|
|
2477
2624
|
app.get('/plugin/shoppe/:identifier/goods', async (req, res) => {
|
|
2478
2625
|
try {
|
|
@@ -2492,8 +2639,14 @@ async function startServer(params) {
|
|
|
2492
2639
|
const tenant = getTenantByIdentifier(req.params.identifier);
|
|
2493
2640
|
if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
|
|
2494
2641
|
const goods = await getShoppeGoods(tenant);
|
|
2642
|
+
|
|
2643
|
+
// Check if the request carries a valid owner signature — if so, embed auth
|
|
2644
|
+
// params in the page so the upload button can authenticate with upload-info.
|
|
2645
|
+
const sigErr = checkOwnerSignature(req, tenant, 24 * 60 * 60 * 1000);
|
|
2646
|
+
const uploadAuth = sigErr ? null : { timestamp: req.query.timestamp, signature: req.query.signature };
|
|
2647
|
+
|
|
2495
2648
|
res.set('Content-Type', 'text/html');
|
|
2496
|
-
res.send(generateShoppeHTML(tenant, goods));
|
|
2649
|
+
res.send(generateShoppeHTML(tenant, goods, uploadAuth));
|
|
2497
2650
|
} catch (err) {
|
|
2498
2651
|
console.error('[shoppe] page error:', err);
|
|
2499
2652
|
res.status(500).send(`<h1>Error</h1><p>${err.message}</p>`);
|