@lvxianchao/dsh-web-guard 2.1.1 → 3.0.1

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/lib/admin.js DELETED
@@ -1,190 +0,0 @@
1
- /**
2
- * Admin page for dsh-web-guard: a server-rendered settings form.
3
- *
4
- * Why server-rendered instead of a harness client module: the plugin stays
5
- * build-free and version-independent. The page is one HTML string plus normal
6
- * form posts, so `dsh plugin add` gives operators a working UI without pulling
7
- * in a bundler or depending on the harness slot API (which is still moving).
8
- */
9
-
10
- import { t as copy, ago } from './i18n.js'
11
-
12
- const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])
13
-
14
- const GITHUB_ICON = `<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`
15
-
16
- const fmtTime = (ms) => {
17
- if (!ms) return '—'
18
- const d = new Date(ms)
19
- const pad = (n) => String(n).padStart(2, '0')
20
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
21
- }
22
-
23
- /**
24
- * Render the admin document.
25
- * @param {object} options
26
- * @param {object} options.settings - effective settings shown in the form
27
- * @param {Array<{ip:string,ua:string,createdAt:number,lastSeen:number,current:boolean}>} options.sessions
28
- * @param {string} [options.notice] - success line
29
- * @param {string} [options.error] - error line
30
- * @param {string} [options.restartNote] - shown when a listened-on value changed
31
- */
32
- export function adminPage({ settings, sessions = [], notice = '', error = '', restartNote = '', locale = 'zh' } = {}) {
33
- const L = copy(locale)
34
- const s = settings ?? {}
35
- const rows = sessions.length === 0
36
- ? `<tr><td colspan="4" class="empty">${esc(L.emptySessions)}</td></tr>`
37
- : sessions.map((x) => `<tr>
38
- <td>${esc(x.ip)}${x.current ? ` <span class="tag">${esc(L.current)}</span>` : ''}</td>
39
- <td class="ua" title="${esc(x.ua)}">${esc(x.ua || '—')}</td>
40
- <td>${fmtTime(x.lastSeen)}<span class="dim"> · ${esc(ago(x.lastSeen, locale))}</span></td>
41
- <td class="num">${x.current ? '' : `<button class="link danger" form="kill" name="id" value="${esc(x.id)}">${esc(L.kick)}</button>`}</td>
42
- </tr>`).join('')
43
-
44
- return `<!doctype html>
45
- <html lang="${locale === 'zh' ? 'zh-CN' : 'en'}"><head>
46
- <meta charset="utf-8">
47
- <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
48
- <meta name="robots" content="noindex,nofollow">
49
- <meta name="color-scheme" content="dark">
50
- <title>${esc(L.pageTitleAdmin)}</title>
51
- <style>
52
- :root{color-scheme:dark;--bg:#0b0d10;--card:#12151a;--line:#24282e;--text:#e6e8eb;--muted:#8b929c;--accent:#3f6df0;--danger:#ff7b72}
53
- *{box-sizing:border-box}
54
- html{background:var(--bg)}
55
- body{margin:0;min-height:100vh;min-height:100dvh;background:var(--bg);color:var(--text);
56
- font:15px/1.65 system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
57
- -webkit-text-size-adjust:100%;padding:28px 18px max(28px,env(safe-area-inset-bottom))}
58
- .wrap{width:min(100%,720px);margin:0 auto;padding-bottom:28px}
59
- header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px;flex-wrap:wrap}
60
- h1{margin:0;font-size:18px;font-weight:600}
61
- h1 a.repo{display:inline-flex;align-items:center;gap:9px;color:inherit;text-decoration:none;transition:color .15s}
62
- h1 a.repo svg{color:var(--muted);transition:color .15s;flex:none}
63
- h1 a.repo:hover{color:#fff}
64
- h1 a.repo:hover svg{color:#fff}
65
- header .links{display:flex;gap:10px;align-items:center;font-size:13px}
66
- header a{display:inline-flex;align-items:center;padding:8px 16px;border-radius:9px;font-size:13px;
67
- font-weight:600;text-decoration:none;line-height:1;border:1px solid transparent;transition:.15s}
68
- header a.primary{background:var(--accent);color:#fff}
69
- header a.primary:hover{background:#4f7bf5}
70
- header a.ghost{border-color:var(--line);color:var(--text)}
71
- header a.ghost:hover{border-color:var(--danger);color:var(--danger)}
72
- .card{border:1px solid var(--line);border-radius:14px;background:var(--card);padding:20px 22px;margin-bottom:16px}
73
- .card h2{margin:0 0 4px;font-size:14px;font-weight:600}
74
- .card p.hint{margin:0 0 16px;color:var(--muted);font-size:12.5px}
75
- .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
76
- .field{display:flex;flex-direction:column;gap:5px;min-width:0}
77
- .field.full{grid-column:1 / -1}
78
- label{font-size:13px;color:#a7aeb8}
79
- input{width:100%;padding:10px 11px;border-radius:9px;border:1px solid #2c3138;background:#0e1116;color:var(--text);
80
- font-size:16px;line-height:1.3;outline:none;transition:border-color .15s,box-shadow .15s}
81
- input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(74,125,255,.18)}
82
- input:disabled{opacity:.55;cursor:not-allowed}
83
- .note{color:#5f666f;font-size:12px}
84
- .actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:4px}
85
- button{padding:10px 18px;border:0;border-radius:9px;background:var(--accent);color:#fff;font-size:14px;font-weight:600;cursor:pointer}
86
- button:hover{background:#4a7dff}
87
- button.ghost{background:#191d22;color:var(--text);border:1px solid var(--line)}
88
- button.ghost:hover{background:#20252c}
89
- button.danger{background:#3a1c1c;color:#ffb4ae;border:1px solid #5a2a2a}
90
- button.danger:hover{background:#4a2222}
91
- button.link{background:none;border:0;color:var(--accent);font-size:13px;padding:2px 4px;cursor:pointer}
92
- button.link.danger{color:var(--danger)}
93
- table{width:100%;border-collapse:collapse;font-size:13px}
94
- th,td{text-align:left;padding:9px 8px;border-bottom:1px solid #1d2126;vertical-align:top}
95
- th{color:var(--muted);font-weight:500;font-size:12px}
96
- tr:last-child td{border-bottom:0}
97
- td.ua{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#a7aeb8}
98
- td.num{text-align:right;white-space:nowrap}
99
- td.empty{text-align:center;color:var(--muted);padding:18px}
100
- .tag{display:inline-block;font-size:11px;padding:1px 6px;border-radius:999px;background:#1d2f52;color:#9db8ff;vertical-align:1px}
101
- .dim{color:#5f666f}
102
- .banner{margin:0 0 16px;padding:10px 14px;border-radius:10px;font-size:13px}
103
- .banner.ok{background:#12271a;color:#8fdca8;border:1px solid #1e4030}
104
- .banner.bad{background:#2a1616;color:#ffb4ae;border:1px solid #4a2222}
105
- .banner.warn{background:#2a2312;color:#e8cf94;border:1px solid #4a3d1e}
106
- body.embedded{background:transparent}
107
- body.embedded .wrap{width:100%;max-width:none;margin:0;padding:0 4px}
108
- body.embedded h1{font-size:15px}
109
- @media (max-width:560px){
110
- .grid{grid-template-columns:1fr}
111
- .card{padding:18px 16px}
112
- td.ua{max-width:120px}
113
- }
114
- </style>
115
- </head><body>
116
- <div class="wrap">
117
- <header>
118
- <h1><a class="repo" href="${esc(s.repo || 'https://github.com/lvxianchao/dsh-web-guard')}" target="_blank" rel="noopener noreferrer" title="GitHub">${GITHUB_ICON}<span>${esc(L.adminTitle)}</span></a></h1>
119
- <div class="links">
120
- <a href="/logout" class="ghost">${esc(L.signOut)}</a>
121
- <a href="/" id="g-back" class="primary">${esc(L.backToDsh)}</a>
122
- </div>
123
- </header>
124
-
125
- ${notice ? `<p class="banner ok">${esc(notice)}</p>` : ''}
126
- ${error ? `<p class="banner bad">${esc(error)}</p>` : ''}
127
- ${restartNote ? `<p class="banner warn">${esc(restartNote)}</p>` : ''}
128
-
129
- <form method="post" action="/__guard/admin" autocomplete="off">
130
- <section class="card">
131
- <h2>${esc(L.secPassword)}</h2>
132
- <p class="hint">${esc(L.hintPassword)}</p>
133
- <div class="grid">
134
- <div class="field full"><label for="current">${esc(L.currentPassword)}</label>
135
- <input id="current" name="current" type="password" autocomplete="current-password" placeholder="${esc(L.currentPasswordPh)}"></div>
136
- <div class="field"><label for="next">${esc(L.newPassword)}</label>
137
- <input id="next" name="next" type="password" autocomplete="new-password" placeholder="${esc(L.newPasswordPh)}"></div>
138
- <div class="field"><label for="confirm">${esc(L.confirmPassword)}</label>
139
- <input id="confirm" name="confirm" type="password" autocomplete="new-password" placeholder="${esc(L.confirmPasswordPh)}"></div>
140
- </div>
141
- </section>
142
-
143
- <section class="card">
144
- <h2>${esc(L.secSession)}</h2>
145
- <p class="hint">${esc(L.hintSession)}</p>
146
- <div class="grid">
147
- <div class="field"><label for="sessionDays">${esc(L.sessionDays)}</label>
148
- <input id="sessionDays" name="sessionDays" type="number" min="0" max="365" step="1" value="${esc(s.sessionDays)}"></div>
149
- <div class="field"><label for="lockMinutes">${esc(L.lockMinutes)}</label>
150
- <input id="lockMinutes" name="lockMinutes" type="number" min="1" max="1440" step="1" value="${esc(s.lockMinutes)}"></div>
151
- <div class="field"><label for="maxAttempts">${esc(L.maxAttempts)}</label>
152
- <input id="maxAttempts" name="maxAttempts" type="number" min="1" max="50" step="1" value="${esc(s.maxAttempts)}"></div>
153
- </div>
154
- </section>
155
-
156
- <section class="card">
157
- <h2>${esc(L.secListen)}</h2>
158
- <p class="hint">${esc(L.hintListen)}</p>
159
- <div class="grid">
160
- <div class="field"><label for="listenHost">${esc(L.listenHost)}</label>
161
- <input id="listenHost" name="listenHost" type="text" value="${esc(s.listenHost)}"></div>
162
- <div class="field"><label for="listenPort">${esc(L.listenPort)}</label>
163
- <input id="listenPort" name="listenPort" type="number" min="1" max="65535" value="${esc(s.listenPort)}"></div>
164
- </div>
165
- </section>
166
-
167
- <div class="actions">
168
- <button type="submit">${esc(L.save)}</button>
169
- <button type="submit" formaction="/__guard/logout-all" formmethod="post" class="ghost"
170
- onclick="return confirm('${esc(L.confirmSignOutAll)}')">${esc(L.signOutAll)}</button>
171
- </div>
172
- </form>
173
-
174
- <section class="card" style="margin-top:16px">
175
- <h2>${esc(L.secSessions)}</h2>
176
- <p class="hint">${esc(L.hintSessions)}</p>
177
- <table>
178
- <thead><tr><th>${esc(L.colSource)}</th><th>${esc(L.colBrowser)}</th><th>${esc(L.colLastSeen)}</th><th></th></tr></thead>
179
- <tbody>${rows}</tbody>
180
- </table>
181
- </section>
182
-
183
- <form id="kill" method="post" action="/__guard/kill"></form>
184
-
185
- </div>
186
- <script>(function(){if(window.self===window.top)return;document.body.classList.add('embedded');
187
- var b=document.getElementById('g-back');if(!b)return;b.textContent='${esc(L.close)}';
188
- b.addEventListener('click',function(e){e.preventDefault();try{parent.postMessage('dsh-web-guard:close',location.origin)}catch(_){}});})()</script>
189
- </body></html>`
190
- }