@a83/orbiter-admin 0.3.13 → 0.3.15
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 +7 -3
- package/public/build.html +1 -1
- package/public/collections.html +1 -1
- package/public/dashboard.html +30 -3
- package/public/editor.html +32 -0
- package/public/import.html +1 -1
- package/public/media.html +1 -1
- package/public/schema.html +19 -1
- package/public/settings.html +76 -6
- package/public/sidebar.js +11 -0
- package/public/style.css +1142 -0
- package/public/theme.js +2 -1
- package/public/users.html +1 -1
- package/public/xfce.js +697 -0
- package/src/cli.js +30 -0
- package/src/email.js +50 -0
- package/src/routes/comments.js +2 -0
- package/src/routes/entries.js +2 -0
- package/src/routes/locks.js +48 -0
- package/src/routes/meta.js +2 -0
- package/src/server.js +2 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// --port flag → PORT env var
|
|
6
|
+
const portIdx = process.argv.indexOf('--port');
|
|
7
|
+
if (portIdx !== -1 && process.argv[portIdx + 1]) {
|
|
8
|
+
process.env.PORT = process.argv[portIdx + 1];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Resolve ORBITER_POD to absolute path (server.js does chdir, so must happen first)
|
|
12
|
+
if (process.env.ORBITER_POD) {
|
|
13
|
+
process.env.ORBITER_POD = resolve(process.env.ORBITER_POD);
|
|
14
|
+
} else {
|
|
15
|
+
// Auto-detect a single .pod file in cwd
|
|
16
|
+
const pods = readdirSync('.').filter(f => f.endsWith('.pod'));
|
|
17
|
+
if (pods.length === 0) {
|
|
18
|
+
console.error('Error: No .pod file found in current directory.');
|
|
19
|
+
console.error('Set ORBITER_POD=/path/to/content.pod or run from the directory containing your .pod file.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (pods.length > 1) {
|
|
23
|
+
console.error('Multiple .pod files found. Specify one with ORBITER_POD:');
|
|
24
|
+
pods.forEach(p => console.error(' ORBITER_POD=' + resolve(p)));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
process.env.ORBITER_POD = resolve(pods[0]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await import('./server.js');
|
package/src/email.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import { openPod } from '@a83/orbiter-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Send a notification email if SMTP is configured.
|
|
6
|
+
* @param {string} podPath
|
|
7
|
+
* @param {string} event — 'publish' | 'comment'
|
|
8
|
+
* @param {object} ctx — { collection, slug, username, body? }
|
|
9
|
+
*/
|
|
10
|
+
export async function sendNotification(podPath, event, ctx = {}) {
|
|
11
|
+
let db;
|
|
12
|
+
try {
|
|
13
|
+
db = openPod(podPath);
|
|
14
|
+
const shouldSend = event === 'publish'
|
|
15
|
+
? db.getMeta('email.notify_publish') === '1'
|
|
16
|
+
: db.getMeta('email.notify_comment') === '1';
|
|
17
|
+
if (!shouldSend) { db.close(); return; }
|
|
18
|
+
|
|
19
|
+
const host = db.getMeta('email.smtp_host') ?? '';
|
|
20
|
+
const port = parseInt(db.getMeta('email.smtp_port') ?? '587', 10);
|
|
21
|
+
const user = db.getMeta('email.smtp_user') ?? '';
|
|
22
|
+
const pass = db.getMeta('email.smtp_pass') ?? '';
|
|
23
|
+
const from = db.getMeta('email.smtp_from') || user;
|
|
24
|
+
const to = db.getMeta('email.notify_to') ?? '';
|
|
25
|
+
const site = db.getMeta('site.name') ?? 'Orbiter';
|
|
26
|
+
db.close();
|
|
27
|
+
|
|
28
|
+
if (!host || !to) return;
|
|
29
|
+
|
|
30
|
+
const transport = nodemailer.createTransport({
|
|
31
|
+
host, port,
|
|
32
|
+
secure: port === 465,
|
|
33
|
+
auth: user ? { user, pass } : undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let subject, text;
|
|
37
|
+
if (event === 'publish') {
|
|
38
|
+
subject = `[${site}] Entry published: ${ctx.collection}/${ctx.slug}`;
|
|
39
|
+
text = `${ctx.username ?? 'Someone'} just published "${ctx.slug}" in the "${ctx.collection}" collection.`;
|
|
40
|
+
} else {
|
|
41
|
+
subject = `[${site}] New comment on ${ctx.collection}/${ctx.slug}`;
|
|
42
|
+
text = `${ctx.username ?? 'Someone'} commented on "${ctx.slug}":\n\n${ctx.body ?? ''}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await transport.sendMail({ from, to, subject, text });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.warn('[email]', e.message);
|
|
48
|
+
db?.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/routes/comments.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { openPod } from '@a83/orbiter-core';
|
|
3
|
+
import { sendNotification } from '../email.js';
|
|
3
4
|
|
|
4
5
|
export const commentRoutes = new Hono();
|
|
5
6
|
|
|
@@ -25,6 +26,7 @@ commentRoutes.post('/:collectionId/entries/:slug/comments', async (c) => {
|
|
|
25
26
|
const username = c.get('user')?.username ?? 'unknown';
|
|
26
27
|
const id = db.createComment(entry.id, username, body.trim());
|
|
27
28
|
db.close();
|
|
29
|
+
sendNotification(c.get('podPath'), 'comment', { collection: collectionId, slug, username, body: body.trim() }).catch(()=>{});
|
|
28
30
|
return c.json({ ok: true, id }, 201);
|
|
29
31
|
});
|
|
30
32
|
|
package/src/routes/entries.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { openPod } from '@a83/orbiter-core';
|
|
3
|
+
import { sendNotification } from '../email.js';
|
|
3
4
|
|
|
4
5
|
export const entryRoutes = new Hono();
|
|
5
6
|
|
|
@@ -108,6 +109,7 @@ entryRoutes.put('/:collectionId/entries/:slug', async (c) => {
|
|
|
108
109
|
|
|
109
110
|
if (body.status === 'published' && before?.status !== 'published') {
|
|
110
111
|
fireWebhook(c.get('podPath'));
|
|
112
|
+
sendNotification(c.get('podPath'), 'publish', { collection: collectionId, slug: body.slug ?? slug, username }).catch(()=>{});
|
|
111
113
|
}
|
|
112
114
|
return c.json(updated);
|
|
113
115
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { openPod } from '@a83/orbiter-core';
|
|
3
|
+
|
|
4
|
+
export const lockRoutes = new Hono();
|
|
5
|
+
|
|
6
|
+
const STALE_MS = 90_000; // lock expires after 90 s without refresh
|
|
7
|
+
|
|
8
|
+
function lockKey(collection, slug) {
|
|
9
|
+
return `lock.${collection}.${slug}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseLock(val) {
|
|
13
|
+
if (!val) return null;
|
|
14
|
+
const [username, ts] = val.split('|');
|
|
15
|
+
const age = Date.now() - new Date(ts).getTime();
|
|
16
|
+
if (age > STALE_MS) return null; // stale — treat as free
|
|
17
|
+
return { username, ts };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// POST /api/locks/:collection/:slug — claim or refresh lock
|
|
21
|
+
lockRoutes.post('/:collection/:slug', (c) => {
|
|
22
|
+
const { collection, slug } = c.req.param();
|
|
23
|
+
const username = c.get('user')?.username ?? 'unknown';
|
|
24
|
+
const db = openPod(c.get('podPath'));
|
|
25
|
+
const key = lockKey(collection, slug);
|
|
26
|
+
const existing = parseLock(db.getMeta(key));
|
|
27
|
+
|
|
28
|
+
if (existing && existing.username !== username) {
|
|
29
|
+
db.close();
|
|
30
|
+
return c.json({ locked: true, by: existing.username }, 409);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
db.setMeta(key, `${username}|${new Date().toISOString()}`);
|
|
34
|
+
db.close();
|
|
35
|
+
return c.json({ locked: false });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// DELETE /api/locks/:collection/:slug — release lock
|
|
39
|
+
lockRoutes.delete('/:collection/:slug', (c) => {
|
|
40
|
+
const { collection, slug } = c.req.param();
|
|
41
|
+
const username = c.get('user')?.username ?? 'unknown';
|
|
42
|
+
const db = openPod(c.get('podPath'));
|
|
43
|
+
const key = lockKey(collection, slug);
|
|
44
|
+
const existing = parseLock(db.getMeta(key));
|
|
45
|
+
if (existing && existing.username === username) db.setMeta(key, '');
|
|
46
|
+
db.close();
|
|
47
|
+
return c.json({ ok: true });
|
|
48
|
+
});
|
package/src/routes/meta.js
CHANGED
|
@@ -15,6 +15,8 @@ const ALLOWED_KEYS = [
|
|
|
15
15
|
'dashboard.notes', 'dashboard.todos',
|
|
16
16
|
'ui.theme',
|
|
17
17
|
'format_version',
|
|
18
|
+
'email.smtp_host', 'email.smtp_port', 'email.smtp_user', 'email.smtp_pass',
|
|
19
|
+
'email.smtp_from', 'email.notify_publish', 'email.notify_comment', 'email.notify_to',
|
|
18
20
|
];
|
|
19
21
|
|
|
20
22
|
const PREVIEW_URL_RE = /^preview_url\.[a-z0-9_-]+$/;
|
package/src/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { githubRoutes } from './routes/github.js';
|
|
|
23
23
|
import { infoRoutes } from './routes/info.js';
|
|
24
24
|
import { importRoutes } from './routes/import.js';
|
|
25
25
|
import { commentRoutes } from './routes/comments.js';
|
|
26
|
+
import { lockRoutes } from './routes/locks.js';
|
|
26
27
|
import { requireAuth } from './middleware/auth.js';
|
|
27
28
|
|
|
28
29
|
const { version: adminVersion } = JSON.parse(
|
|
@@ -74,6 +75,7 @@ export function createApp(podPath) {
|
|
|
74
75
|
api.route('/import', importRoutes);
|
|
75
76
|
api.route('/collections', commentRoutes);
|
|
76
77
|
api.route('/', commentRoutes);
|
|
78
|
+
api.route('/locks', lockRoutes);
|
|
77
79
|
|
|
78
80
|
app.route('/api', api);
|
|
79
81
|
|