@a83/orbiter-admin 0.3.49 → 0.3.50
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 +2 -1
- package/public/build.html +72 -31
- package/public/settings.html +60 -0
- package/src/routes/build.js +7 -0
- package/src/routes/deploy.js +94 -0
- package/src/routes/meta.js +2 -0
- package/src/server.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a83/orbiter-admin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.50",
|
|
4
4
|
"description": "Standalone admin server for Orbiter CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@a83/orbiter-core": "^0.3.2",
|
|
33
|
+
"basic-ftp": "^5.0.5",
|
|
33
34
|
"@hono/node-server": "^1.14.4",
|
|
34
35
|
"hono": "^4.7.11",
|
|
35
36
|
"nodemailer": "^8.0.10",
|
package/public/build.html
CHANGED
|
@@ -10,20 +10,25 @@
|
|
|
10
10
|
<link rel="stylesheet" href="/style.css" />
|
|
11
11
|
<script src="/theme.js"></script>
|
|
12
12
|
<style>
|
|
13
|
-
.build-
|
|
14
|
-
.build-
|
|
13
|
+
.build-cards { display:flex; flex-direction:column; gap:16px; max-width:560px; }
|
|
14
|
+
.build-card { background:var(--bg2); border:1px solid var(--line); border-radius:var(--radius); padding:24px 28px; }
|
|
15
|
+
.build-card-header { font-size:10px; letter-spacing:.16em; text-transform:uppercase; color:var(--muted); margin-bottom:16px; display:flex; align-items:center; gap:7px; }
|
|
16
|
+
.build-card-header::before { content:"◆"; color:var(--gold); font-size:5px; }
|
|
17
|
+
.build-status { display:flex; align-items:center; gap:10px; margin-bottom:14px; }
|
|
15
18
|
.build-dot { width:8px; height:8px; border-radius:50%; background:var(--line); flex-shrink:0; }
|
|
16
|
-
.build-dot.
|
|
17
|
-
.build-dot.
|
|
19
|
+
.build-dot.ok { background:var(--jade); }
|
|
20
|
+
.build-dot.err { background:var(--red); }
|
|
21
|
+
.build-dot.off { background:var(--muted); }
|
|
18
22
|
.build-label { font-size:12px; color:var(--text); }
|
|
19
|
-
.build-meta { font-size:10px; color:var(--muted); margin-bottom:
|
|
20
|
-
.build-last { font-size:10px; color:var(--muted); margin-top:
|
|
21
|
-
.
|
|
23
|
+
.build-meta { font-size:10px; color:var(--muted); margin-bottom:20px; }
|
|
24
|
+
.build-last { font-size:10px; color:var(--muted); margin-top:10px; }
|
|
25
|
+
.build-err { font-size:10px; color:var(--red); margin-top:6px; font-family:var(--mono); white-space:pre-wrap; word-break:break-all; }
|
|
26
|
+
.banner { padding:8px 16px; font-size:10px; display:flex; align-items:center; gap:6px; border-radius:var(--radius); margin-bottom:14px; max-width:560px; }
|
|
22
27
|
.banner-ok { background:var(--jade-bg); color:var(--jade); border:1px solid rgba(45,139,106,.2); }
|
|
23
28
|
.banner-ok::before { content:"✓"; }
|
|
24
29
|
.banner-err { background:rgba(139,38,53,.07); color:var(--red); border:1px solid rgba(139,38,53,.15); }
|
|
25
30
|
.banner-err::before { content:"✕"; }
|
|
26
|
-
.config-hint { font-size:11px; color:var(--muted); margin-top:
|
|
31
|
+
.config-hint { font-size:11px; color:var(--muted); margin-top:10px; line-height:1.7; }
|
|
27
32
|
.config-hint a { color:var(--accent); text-decoration:none; }
|
|
28
33
|
.config-hint a:hover { text-decoration:underline; }
|
|
29
34
|
</style>
|
|
@@ -86,40 +91,76 @@
|
|
|
86
91
|
setTimeout(()=>el.style.display='none', 4000);
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
const status = await
|
|
94
|
+
const [status, ftp] = await Promise.all([
|
|
95
|
+
fetch('/api/build/status', {credentials:'include'}).then(r=>r.json()).catch(()=>({configured:false,lastTriggered:null})),
|
|
96
|
+
fetch('/api/deploy/ftp/status', {credentials:'include'}).then(r=>r.json()).catch(()=>({configured:false})),
|
|
97
|
+
]);
|
|
98
|
+
|
|
90
99
|
const lastEl = document.getElementById('last-triggered');
|
|
91
|
-
lastEl.textContent = status.lastTriggered ? `Last: ${new Date(status.lastTriggered).toLocaleString()}` : '
|
|
100
|
+
lastEl.textContent = status.lastTriggered ? `Last build: ${new Date(status.lastTriggered).toLocaleString()}` : '';
|
|
92
101
|
|
|
93
102
|
const wrap = document.getElementById('content');
|
|
94
103
|
wrap.innerHTML = `
|
|
95
|
-
<div class="build-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
<div class="build-cards">
|
|
105
|
+
|
|
106
|
+
<!-- Build webhook card -->
|
|
107
|
+
<div class="build-card">
|
|
108
|
+
<div class="build-card-header">Build webhook</div>
|
|
109
|
+
<div class="build-status">
|
|
110
|
+
<div class="build-dot ${status.configured?'ok':'off'}"></div>
|
|
111
|
+
<div class="build-label">${status.configured ? 'Webhook configured' : 'No webhook configured'}</div>
|
|
112
|
+
</div>
|
|
113
|
+
${status.configured
|
|
114
|
+
? `<div class="build-meta">POSTs to the webhook URL to trigger a build on Netlify, Vercel, or GitHub Actions.</div>
|
|
115
|
+
<button class="btn btn-primary" id="trigger-btn" style="min-width:160px;">▲ Trigger build</button>
|
|
116
|
+
${status.lastTriggered ? `<div class="build-last">Last triggered: ${new Date(status.lastTriggered).toLocaleString()}</div>` : ''}`
|
|
117
|
+
: `<div class="config-hint">No webhook URL configured. Add one in <a href="/settings.html">Settings → Build</a>.</div>`
|
|
118
|
+
}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- FTP deploy card -->
|
|
122
|
+
<div class="build-card">
|
|
123
|
+
<div class="build-card-header">FTP / FTPS Deploy</div>
|
|
124
|
+
<div class="build-status">
|
|
125
|
+
<div class="build-dot ${ftp.configured ? (ftp.lastStatus==='error'?'err':'ok') : 'off'}"></div>
|
|
126
|
+
<div class="build-label">${ftp.configured
|
|
127
|
+
? (ftp.lastStatus==='error' ? 'Last deploy failed' : ftp.lastDeploy ? 'Configured' : 'Configured — never deployed')
|
|
128
|
+
: 'Not configured'}</div>
|
|
129
|
+
</div>
|
|
130
|
+
${ftp.configured
|
|
131
|
+
? `<div class="build-meta">Uploads your Astro <code style="font-family:var(--mono);font-size:10px;background:var(--bg3);padding:1px 5px;border-radius:3px">dist/</code> folder directly to your FTP server (World4You, Strato, etc.).</div>
|
|
132
|
+
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
133
|
+
<button class="btn btn-primary" id="ftp-deploy-btn" style="min-width:160px;">↑ Deploy via FTP</button>
|
|
134
|
+
${ftp.autoDeploy ? '<span style="font-size:10px;color:var(--muted)">Auto-deploy after build: on</span>' : ''}
|
|
135
|
+
</div>
|
|
136
|
+
${ftp.lastDeploy ? `<div class="build-last">Last deploy: ${new Date(ftp.lastDeploy).toLocaleString()}</div>` : ''}
|
|
137
|
+
${ftp.lastStatus==='error' && ftp.lastError ? `<div class="build-err">${ftp.lastError}</div>` : ''}`
|
|
138
|
+
: `<div class="config-hint">Configure FTP credentials in <a href="/settings.html">Settings → FTP / FTPS Deploy</a> to upload directly to shared hosting.</div>`
|
|
139
|
+
}
|
|
99
140
|
</div>
|
|
100
|
-
|
|
101
|
-
? `<div class="build-meta">Triggering a build will POST to the configured webhook URL.</div>
|
|
102
|
-
<button class="btn btn-primary" id="trigger-btn" style="min-width:160px;">▲ Trigger build</button>
|
|
103
|
-
${status.lastTriggered ? `<div class="build-last">Last triggered: ${new Date(status.lastTriggered).toLocaleString()}</div>` : ''}`
|
|
104
|
-
: `<div class="config-hint">
|
|
105
|
-
No webhook URL is configured. To enable build triggers, add a webhook URL in
|
|
106
|
-
<a href="/settings.html">Settings → Build</a>.
|
|
107
|
-
</div>`
|
|
108
|
-
}
|
|
141
|
+
|
|
109
142
|
</div>
|
|
110
143
|
`;
|
|
111
144
|
|
|
112
|
-
document.getElementById('trigger-btn')?.addEventListener('click', async
|
|
113
|
-
btn = document.getElementById('trigger-btn');
|
|
114
|
-
btn.disabled = true;
|
|
115
|
-
|
|
116
|
-
const res = await fetch('/api/build/trigger',{method:'POST',credentials:'include'});
|
|
145
|
+
document.getElementById('trigger-btn')?.addEventListener('click', async () => {
|
|
146
|
+
const btn = document.getElementById('trigger-btn');
|
|
147
|
+
btn.disabled = true; btn.textContent = 'Triggering…';
|
|
148
|
+
const res = await fetch('/api/build/trigger', {method:'POST', credentials:'include'});
|
|
117
149
|
const json = await res.json();
|
|
118
|
-
btn.disabled = false;
|
|
119
|
-
|
|
120
|
-
if (res.ok) showBanner('banner-ok','Build triggered');
|
|
150
|
+
btn.disabled = false; btn.textContent = '▲ Trigger build';
|
|
151
|
+
if (res.ok) showBanner('banner-ok', 'Build triggered');
|
|
121
152
|
else showBanner('banner-err', json.error ?? 'Webhook error');
|
|
122
153
|
});
|
|
154
|
+
|
|
155
|
+
document.getElementById('ftp-deploy-btn')?.addEventListener('click', async () => {
|
|
156
|
+
const btn = document.getElementById('ftp-deploy-btn');
|
|
157
|
+
btn.disabled = true; btn.textContent = '↑ Uploading…';
|
|
158
|
+
const res = await fetch('/api/deploy/ftp', {method:'POST', credentials:'include'});
|
|
159
|
+
const json = await res.json();
|
|
160
|
+
btn.disabled = false; btn.textContent = '↑ Deploy via FTP';
|
|
161
|
+
if (res.ok) showBanner('banner-ok', 'FTP deploy complete');
|
|
162
|
+
else showBanner('banner-err', json.error ?? 'FTP error');
|
|
163
|
+
});
|
|
123
164
|
</script>
|
|
124
165
|
</main>
|
|
125
166
|
</div>
|
package/public/settings.html
CHANGED
|
@@ -502,6 +502,46 @@
|
|
|
502
502
|
</div>
|
|
503
503
|
</div>
|
|
504
504
|
|
|
505
|
+
<div class="settings-group" style="grid-column:1/-1">
|
|
506
|
+
<div class="group-header">FTP / FTPS Deploy</div>
|
|
507
|
+
<div class="setting-row">
|
|
508
|
+
<div><div class="setting-label">FTP host</div><div class="setting-desc">e.g. ftp.world4you.com or ftp.strato.de</div></div>
|
|
509
|
+
<input class="input" name="ftp.host" value="${get('ftp.host')||''}" placeholder="ftp.example.com" autocomplete="off" />
|
|
510
|
+
</div>
|
|
511
|
+
<div class="setting-row">
|
|
512
|
+
<div><div class="setting-label">Port</div><div class="setting-desc">21 for FTP (default), 990 for implicit FTPS</div></div>
|
|
513
|
+
<input class="input" name="ftp.port" type="number" value="${get('ftp.port')||'21'}" placeholder="21" style="max-width:100px" />
|
|
514
|
+
</div>
|
|
515
|
+
<div class="setting-row">
|
|
516
|
+
<div><div class="setting-label">Username</div></div>
|
|
517
|
+
<input class="input" name="ftp.user" value="${get('ftp.user')||''}" placeholder="u12345678" autocomplete="off" />
|
|
518
|
+
</div>
|
|
519
|
+
<div class="setting-row">
|
|
520
|
+
<div><div class="setting-label">Password</div></div>
|
|
521
|
+
<input class="input" type="password" name="ftp.password" value="${get('ftp.password')||''}" autocomplete="off" />
|
|
522
|
+
</div>
|
|
523
|
+
<div class="setting-row">
|
|
524
|
+
<div><div class="setting-label">Use FTPS</div><div class="setting-desc">Secure FTP over TLS (explicit mode)</div></div>
|
|
525
|
+
<input type="checkbox" name="ftp.secure" value="1" ${get('ftp.secure')==='1'?'checked':''} style="accent-color:var(--accent);width:14px;height:14px;" />
|
|
526
|
+
</div>
|
|
527
|
+
<div class="setting-row">
|
|
528
|
+
<div><div class="setting-label">Remote path</div><div class="setting-desc">Target folder on the server</div></div>
|
|
529
|
+
<input class="input" name="ftp.remote_path" value="${get('ftp.remote_path')||''}" placeholder="/public_html" />
|
|
530
|
+
</div>
|
|
531
|
+
<div class="setting-row">
|
|
532
|
+
<div><div class="setting-label">Local dist path</div><div class="setting-desc">Absolute path to your Astro <code style="font-family:var(--mono);font-size:10px;background:var(--bg3);padding:1px 5px;border-radius:3px">dist/</code> folder on this machine</div></div>
|
|
533
|
+
<input class="input" name="ftp.local_path" value="${get('ftp.local_path')||''}" placeholder="/Users/me/my-site/dist" />
|
|
534
|
+
</div>
|
|
535
|
+
<div class="setting-row">
|
|
536
|
+
<div><div class="setting-label">Auto-deploy after build</div><div class="setting-desc">Upload via FTP automatically after a successful build webhook</div></div>
|
|
537
|
+
<input type="checkbox" name="ftp.auto_deploy" value="1" ${get('ftp.auto_deploy')==='1'?'checked':''} style="accent-color:var(--accent);width:14px;height:14px;" />
|
|
538
|
+
</div>
|
|
539
|
+
<div class="save-row" style="justify-content:flex-start;gap:14px">
|
|
540
|
+
<button type="button" class="btn btn-ghost" id="ftp-test-btn">Test connection</button>
|
|
541
|
+
<span id="ftp-test-result" style="font-size:11px;color:var(--muted)"></span>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
505
545
|
</div><!-- /settings-grid -->
|
|
506
546
|
</form>
|
|
507
547
|
|
|
@@ -759,6 +799,14 @@
|
|
|
759
799
|
['email.notify_to', fd.get('email.notify_to')],
|
|
760
800
|
['email.notify_publish', fd.get('email.notify_publish') ? '1' : '0'],
|
|
761
801
|
['email.notify_comment', fd.get('email.notify_comment') ? '1' : '0'],
|
|
802
|
+
['ftp.host', fd.get('ftp.host')],
|
|
803
|
+
['ftp.port', fd.get('ftp.port')],
|
|
804
|
+
['ftp.user', fd.get('ftp.user')],
|
|
805
|
+
['ftp.password', fd.get('ftp.password')],
|
|
806
|
+
['ftp.secure', fd.get('ftp.secure') ? '1' : '0'],
|
|
807
|
+
['ftp.remote_path', fd.get('ftp.remote_path')],
|
|
808
|
+
['ftp.local_path', fd.get('ftp.local_path')],
|
|
809
|
+
['ftp.auto_deploy', fd.get('ftp.auto_deploy') ? '1' : '0'],
|
|
762
810
|
]);
|
|
763
811
|
showBanner('site-banner','banner-ok','Settings saved');
|
|
764
812
|
});
|
|
@@ -831,6 +879,18 @@
|
|
|
831
879
|
});
|
|
832
880
|
});
|
|
833
881
|
|
|
882
|
+
// FTP test connection
|
|
883
|
+
document.getElementById('ftp-test-btn')?.addEventListener('click', async () => {
|
|
884
|
+
const btn = document.getElementById('ftp-test-btn');
|
|
885
|
+
const res = document.getElementById('ftp-test-result');
|
|
886
|
+
btn.disabled = true; btn.textContent = 'Testing…'; res.textContent = '';
|
|
887
|
+
const r = await fetch('/api/deploy/ftp/test', { method: 'POST', credentials: 'include' });
|
|
888
|
+
const j = await r.json();
|
|
889
|
+
btn.disabled = false; btn.textContent = 'Test connection';
|
|
890
|
+
if (r.ok) { res.style.color = 'var(--jade)'; res.textContent = `✓ Connected — ${j.files} files in ${j.path}`; }
|
|
891
|
+
else { res.style.color = 'var(--red)'; res.textContent = `✕ ${j.error}`; }
|
|
892
|
+
});
|
|
893
|
+
|
|
834
894
|
// Password form (with confirm validation)
|
|
835
895
|
document.getElementById('pw-form').addEventListener('submit', async e => {
|
|
836
896
|
e.preventDefault();
|
package/src/routes/build.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { openPod } from '@a83/orbiter-core';
|
|
3
|
+
import { runFtpDeploy } from './deploy.js';
|
|
3
4
|
|
|
4
5
|
export const buildRoutes = new Hono();
|
|
5
6
|
|
|
@@ -12,6 +13,12 @@ buildRoutes.post('/trigger', async (c) => {
|
|
|
12
13
|
|
|
13
14
|
const res = await fetch(url, { method: 'POST' }).catch(e => ({ ok: false, status: 0, err: e.message }));
|
|
14
15
|
if (!res.ok) return c.json({ error: `Webhook returned ${res.status}` }, 502);
|
|
16
|
+
|
|
17
|
+
const db2 = openPod(c.get('podPath'));
|
|
18
|
+
const autoDeploy = db2.getMeta('ftp.auto_deploy') === '1';
|
|
19
|
+
db2.close();
|
|
20
|
+
if (autoDeploy) runFtpDeploy(c.get('podPath')).catch(e => console.warn('[ftp-auto-deploy]', e.message));
|
|
21
|
+
|
|
15
22
|
return c.json({ ok: true });
|
|
16
23
|
});
|
|
17
24
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { Client } from 'basic-ftp';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { openPod } from '@a83/orbiter-core';
|
|
5
|
+
|
|
6
|
+
export const deployRoutes = new Hono();
|
|
7
|
+
|
|
8
|
+
async function getFtpCfg(podPath) {
|
|
9
|
+
const db = openPod(podPath);
|
|
10
|
+
const cfg = {
|
|
11
|
+
host: db.getMeta('ftp.host') ?? '',
|
|
12
|
+
port: parseInt(db.getMeta('ftp.port') ?? '21', 10),
|
|
13
|
+
user: db.getMeta('ftp.user') ?? '',
|
|
14
|
+
password: db.getMeta('ftp.password') ?? '',
|
|
15
|
+
remotePath: db.getMeta('ftp.remote_path') ?? '/',
|
|
16
|
+
localPath: db.getMeta('ftp.local_path') ?? '',
|
|
17
|
+
secure: db.getMeta('ftp.secure') === '1',
|
|
18
|
+
};
|
|
19
|
+
db.close();
|
|
20
|
+
return cfg;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runFtpDeploy(podPath) {
|
|
24
|
+
const cfg = await getFtpCfg(podPath);
|
|
25
|
+
if (!cfg.host || !cfg.user || !cfg.password || !cfg.localPath) {
|
|
26
|
+
throw new Error('FTP not fully configured');
|
|
27
|
+
}
|
|
28
|
+
if (!existsSync(cfg.localPath)) {
|
|
29
|
+
throw new Error(`Local path not found: ${cfg.localPath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const client = new Client();
|
|
33
|
+
client.ftp.verbose = false;
|
|
34
|
+
try {
|
|
35
|
+
await client.access({ host: cfg.host, port: cfg.port, user: cfg.user, password: cfg.password, secure: cfg.secure });
|
|
36
|
+
await client.uploadFromDir(cfg.localPath, cfg.remotePath || '/');
|
|
37
|
+
|
|
38
|
+
const db = openPod(podPath);
|
|
39
|
+
db.setMeta('ftp.last_deploy', new Date().toISOString());
|
|
40
|
+
db.setMeta('ftp.last_status', 'ok');
|
|
41
|
+
db.setMeta('ftp.last_error', '');
|
|
42
|
+
db.close();
|
|
43
|
+
} finally {
|
|
44
|
+
client.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GET /api/deploy/ftp/status
|
|
49
|
+
deployRoutes.get('/ftp/status', (c) => {
|
|
50
|
+
const db = openPod(c.get('podPath'));
|
|
51
|
+
const out = {
|
|
52
|
+
configured: !!(db.getMeta('ftp.host') && db.getMeta('ftp.user') && db.getMeta('ftp.password') && db.getMeta('ftp.local_path')),
|
|
53
|
+
lastDeploy: db.getMeta('ftp.last_deploy') ?? null,
|
|
54
|
+
lastStatus: db.getMeta('ftp.last_status') ?? null,
|
|
55
|
+
lastError: db.getMeta('ftp.last_error') ?? null,
|
|
56
|
+
autoDeploy: db.getMeta('ftp.auto_deploy') === '1',
|
|
57
|
+
};
|
|
58
|
+
db.close();
|
|
59
|
+
return c.json(out);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// POST /api/deploy/ftp — run full deploy
|
|
63
|
+
deployRoutes.post('/ftp', async (c) => {
|
|
64
|
+
try {
|
|
65
|
+
await runFtpDeploy(c.get('podPath'));
|
|
66
|
+
return c.json({ ok: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const db = openPod(c.get('podPath'));
|
|
69
|
+
db.setMeta('ftp.last_status', 'error');
|
|
70
|
+
db.setMeta('ftp.last_error', err.message);
|
|
71
|
+
db.close();
|
|
72
|
+
return c.json({ error: err.message }, 502);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// POST /api/deploy/ftp/test — test connection only, no upload
|
|
77
|
+
deployRoutes.post('/ftp/test', async (c) => {
|
|
78
|
+
const cfg = await getFtpCfg(c.get('podPath'));
|
|
79
|
+
if (!cfg.host || !cfg.user || !cfg.password) {
|
|
80
|
+
return c.json({ error: 'FTP credentials not configured — save settings first' }, 400);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const client = new Client();
|
|
84
|
+
client.ftp.verbose = false;
|
|
85
|
+
try {
|
|
86
|
+
await client.access({ host: cfg.host, port: cfg.port, user: cfg.user, password: cfg.password, secure: cfg.secure });
|
|
87
|
+
const list = await client.list(cfg.remotePath || '/');
|
|
88
|
+
return c.json({ ok: true, files: list.length, path: cfg.remotePath || '/' });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return c.json({ error: err.message }, 502);
|
|
91
|
+
} finally {
|
|
92
|
+
client.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
package/src/routes/meta.js
CHANGED
|
@@ -17,6 +17,8 @@ const ALLOWED_KEYS = [
|
|
|
17
17
|
'format_version',
|
|
18
18
|
'email.smtp_host', 'email.smtp_port', 'email.smtp_user', 'email.smtp_pass',
|
|
19
19
|
'email.smtp_from', 'email.notify_publish', 'email.notify_comment', 'email.notify_to',
|
|
20
|
+
'ftp.host', 'ftp.port', 'ftp.user', 'ftp.password',
|
|
21
|
+
'ftp.remote_path', 'ftp.local_path', 'ftp.secure', 'ftp.auto_deploy',
|
|
20
22
|
];
|
|
21
23
|
|
|
22
24
|
const PREVIEW_URL_RE = /^preview_url\.[a-z0-9_-]+$/;
|
package/src/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { importRoutes } from './routes/import.js';
|
|
|
25
25
|
import { commentRoutes } from './routes/comments.js';
|
|
26
26
|
import { lockRoutes } from './routes/locks.js';
|
|
27
27
|
import { terminalRoutes } from './routes/terminal.js';
|
|
28
|
+
import { deployRoutes } from './routes/deploy.js';
|
|
28
29
|
import { requireAuth } from './middleware/auth.js';
|
|
29
30
|
import { csrfMiddleware } from './middleware/csrf.js';
|
|
30
31
|
|
|
@@ -80,6 +81,7 @@ export function createApp(podPath) {
|
|
|
80
81
|
api.route('/', commentRoutes);
|
|
81
82
|
api.route('/locks', lockRoutes);
|
|
82
83
|
api.route('/terminal', terminalRoutes);
|
|
84
|
+
api.route('/deploy', deployRoutes);
|
|
83
85
|
|
|
84
86
|
app.route('/api', api);
|
|
85
87
|
|