@a83/orbiter-admin 0.3.5 → 0.3.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a83/orbiter-admin",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Standalone admin server for Orbiter CMS",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
@@ -38,6 +38,10 @@
38
38
  .section-label { font-size:9px; letter-spacing:0.28em; text-transform:uppercase; color:var(--muted); margin-bottom:14px; display:flex; align-items:center; gap:8px; }
39
39
  .section-label::before { content:"—"; color:var(--gold); }
40
40
  .field-rows { background:var(--bg2); border:1px solid var(--line); margin-bottom:14px; border-radius:var(--radius); overflow:hidden; min-height:10px; }
41
+ .fld-drag { cursor:grab; color:var(--muted); opacity:0.4; font-size:13px; padding:0 4px; user-select:none; }
42
+ .fld-drag:hover { opacity:0.9; }
43
+ .fld-row-dragging { opacity:0.4; }
44
+ .fld-row-over { outline:2px solid var(--accent); outline-offset:-2px; }
41
45
  .editor-actions { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:20px 0 0; border-top:1px solid var(--line); margin-top:20px; }
42
46
  .banner { padding:8px 16px; font-size:10px; display:flex; align-items:center; gap:6px; border-radius:var(--radius); margin-bottom:14px; }
43
47
  .banner-ok::before { content:"✓ "; }
@@ -163,7 +167,7 @@
163
167
  }
164
168
 
165
169
  const S = {
166
- row: 'display:grid;grid-template-columns:1fr 260px;align-items:start;gap:20px;padding:14px 18px;border-bottom:1px solid var(--line2);background:var(--bg2);',
170
+ row: 'display:grid;grid-template-columns:20px 1fr 260px;align-items:start;gap:16px;padding:14px 18px 14px 10px;border-bottom:1px solid var(--line2);background:var(--bg2);',
167
171
  input: 'width:100%;background:var(--bg0);border:1px solid var(--line);padding:7px 9px;color:var(--heading);font-family:var(--mono);font-size:11px;outline:none;appearance:none;box-sizing:border-box;',
168
172
  iKey: 'width:100%;background:var(--bg0);border:1px solid var(--line);padding:7px 9px;color:var(--heading);font-family:var(--mono);font-size:12px;outline:none;box-sizing:border-box;',
169
173
  iLbl: 'width:100%;background:var(--bg0);border:1px solid var(--line);padding:6px 9px;color:var(--muted);font-family:var(--mono);font-size:10px;outline:none;box-sizing:border-box;',
@@ -185,7 +189,9 @@
185
189
  function addFieldRow(container, key='', label='', type='string', required=false, options='', collection='', multiple=true) {
186
190
  const div = document.createElement('div');
187
191
  div.style.cssText = S.row;
192
+ div.setAttribute('draggable','true');
188
193
  div.innerHTML = `
194
+ <span class="fld-drag" title="Drag to reorder">⠿</span>
189
195
  <div style="display:flex;flex-direction:column;gap:6px;">
190
196
  <input class="f-key" style="${S.iKey}" placeholder="field_key" value="${escHtml(key)}" />
191
197
  <input class="f-label" style="${S.iLbl}" placeholder="Field label" value="${escHtml(label)}" />
@@ -237,6 +243,40 @@
237
243
  container.appendChild(div);
238
244
  }
239
245
 
246
+ function enableFieldDrag(container) {
247
+ let dragSrc = null;
248
+ container.addEventListener('dragstart', e => {
249
+ const row = e.target.closest('[draggable]');
250
+ if (!row || !row.querySelector('.fld-drag')?.contains(e.target) && e.target.className !== 'fld-drag') { e.preventDefault(); return; }
251
+ dragSrc = row;
252
+ row.classList.add('fld-row-dragging');
253
+ e.dataTransfer.effectAllowed = 'move';
254
+ });
255
+ container.addEventListener('dragend', () => {
256
+ dragSrc?.classList.remove('fld-row-dragging');
257
+ container.querySelectorAll('.fld-row-over').forEach(r => r.classList.remove('fld-row-over'));
258
+ dragSrc = null;
259
+ });
260
+ container.addEventListener('dragover', e => {
261
+ e.preventDefault();
262
+ const row = e.target.closest('[draggable]');
263
+ container.querySelectorAll('.fld-row-over').forEach(r => r.classList.remove('fld-row-over'));
264
+ if (row && row !== dragSrc) row.classList.add('fld-row-over');
265
+ });
266
+ container.addEventListener('drop', e => {
267
+ e.preventDefault();
268
+ const row = e.target.closest('[draggable]');
269
+ if (!dragSrc || !row || row === dragSrc) return;
270
+ row.classList.remove('fld-row-over');
271
+ const rows = [...container.children];
272
+ const from = rows.indexOf(dragSrc);
273
+ const to = rows.indexOf(row);
274
+ container.removeChild(dragSrc);
275
+ if (from < to) row.after(dragSrc);
276
+ else row.before(dragSrc);
277
+ });
278
+ }
279
+
240
280
  function serializeSchema(container) {
241
281
  const schema = {};
242
282
  container.querySelectorAll(':scope > div[style]').forEach(row=>{
@@ -296,8 +336,10 @@
296
336
  document.getElementById('new-col-id').addEventListener('input',e=>{
297
337
  e.target.value=e.target.value.toLowerCase().replace(/[^a-z0-9_]/g,'_').replace(/^_+/,'');
298
338
  });
299
- document.getElementById('btn-add-new').addEventListener('click',()=>addFieldRow(document.getElementById('new-field-rows')));
300
- addFieldRow(document.getElementById('new-field-rows'),'title','Title','string',true);
339
+ const newRowsEl = document.getElementById('new-field-rows');
340
+ enableFieldDrag(newRowsEl);
341
+ document.getElementById('btn-add-new').addEventListener('click',()=>addFieldRow(newRowsEl));
342
+ addFieldRow(newRowsEl,'title','Title','string',true);
301
343
  document.getElementById('btn-save-new').addEventListener('click',async()=>{
302
344
  const id=document.getElementById('new-col-id').value.trim();
303
345
  const label=document.getElementById('new-col-label').value.trim();
@@ -356,6 +398,7 @@
356
398
  </div>
357
399
  `;
358
400
  const rowsEl=document.getElementById('edit-field-rows');
401
+ enableFieldDrag(rowsEl);
359
402
  Object.entries(schema).forEach(([k,f])=>addFieldRow(rowsEl,k,f.label??k,f.type??'string',!!f.required,(f.options??[]).join(', '),f.collection??'',f.multiple!==false));
360
403
  document.getElementById('btn-add-edit').addEventListener('click',()=>addFieldRow(rowsEl));
361
404
  // Load existing preview URL for this collection
@@ -4,8 +4,40 @@ import { openPod, verifyPassword, generateToken } from '@a83/orbiter-core';
4
4
 
5
5
  export const authRoutes = new Hono();
6
6
 
7
+ const LOGIN_MAX = 5;
8
+ const LOGIN_WINDOW = 15 * 60 * 1000; // 15 min
9
+ const loginAttempts = new Map(); // ip → { count, resetAt }
10
+
11
+ function getRealIp(c) {
12
+ return c.req.header('x-forwarded-for')?.split(',')[0].trim()
13
+ ?? c.req.header('x-real-ip')
14
+ ?? 'unknown';
15
+ }
16
+
17
+ function checkRateLimit(ip) {
18
+ const now = Date.now();
19
+ const rec = loginAttempts.get(ip);
20
+ if (rec && rec.resetAt > now && rec.count >= LOGIN_MAX) return false;
21
+ if (!rec || rec.resetAt <= now) loginAttempts.set(ip, { count: 0, resetAt: now + LOGIN_WINDOW });
22
+ return true;
23
+ }
24
+
25
+ function recordFailure(ip) {
26
+ const rec = loginAttempts.get(ip);
27
+ if (rec) rec.count++;
28
+ }
29
+
30
+ function clearAttempts(ip) {
31
+ loginAttempts.delete(ip);
32
+ }
33
+
7
34
  // POST /api/auth/login
8
35
  authRoutes.post('/login', async (c) => {
36
+ const ip = getRealIp(c);
37
+ if (!checkRateLimit(ip)) {
38
+ return c.json({ error: 'Too many login attempts. Try again in 15 minutes.' }, 429);
39
+ }
40
+
9
41
  const { username, password } = await c.req.json();
10
42
  if (!username || !password) return c.json({ error: 'Missing credentials' }, 400);
11
43
 
@@ -13,8 +45,10 @@ authRoutes.post('/login', async (c) => {
13
45
  const user = db.getUserByUsername(username);
14
46
  if (!user || !(await verifyPassword(password, user.password))) {
15
47
  db.close();
48
+ recordFailure(ip);
16
49
  return c.json({ error: 'Invalid username or password' }, 401);
17
50
  }
51
+ clearAttempts(ip);
18
52
 
19
53
  const token = generateToken();
20
54
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)