orbit-codex-bridge 0.1.6 → 0.1.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/README.md +20 -2
- package/cli.mjs +20 -3
- package/lib/approvals.mjs +97 -0
- package/lib/client-files.mjs +37 -0
- package/lib/connector-service.mjs +7 -3
- package/lib/desktop.mjs +18 -24
- package/lib/resume-manager.mjs +39 -7
- package/lib/rpc.mjs +40 -21
- package/lib/store.mjs +6 -0
- package/lib/updates.mjs +97 -0
- package/package.json +19 -18
- package/relay-agent.mjs +2 -4
- package/scripts/verify-client-package.mjs +34 -0
- package/server.mjs +69 -126
- package/lib/device-auth.mjs +0 -69
- package/public/admin.css +0 -67
- package/public/admin.html +0 -23
- package/public/admin.js +0 -186
- package/public/app.js +0 -1382
- package/public/connect.html +0 -56
- package/public/connect.js +0 -126
- package/public/goal.css +0 -7
- package/public/icon.svg +0 -1
- package/public/index.html +0 -60
- package/public/layout.css +0 -73
- package/public/manifest.webmanifest +0 -1
- package/public/style.css +0 -70
- package/relay-server.mjs +0 -703
- package/relay-store.mjs +0 -368
package/public/admin.css
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
:root{font-family:Inter,"Segoe UI","Microsoft YaHei",sans-serif;color:#27392f;background:#f5f7f2;font-size:14px;--line:#e3e8de;--green:#183c34;--accent:#d8edb2}
|
|
2
|
-
*{box-sizing:border-box}
|
|
3
|
-
body{margin:0}
|
|
4
|
-
button,input,textarea{font:inherit}
|
|
5
|
-
button{cursor:pointer;border:1px solid var(--line);background:#fff;color:inherit;border-radius:5px;padding:8px 12px}
|
|
6
|
-
button:disabled{opacity:.55;cursor:not-allowed}
|
|
7
|
-
[hidden]{display:none!important}
|
|
8
|
-
.admin-shell{width:min(1240px,calc(100% - 40px));margin:0 auto}
|
|
9
|
-
.admin-top{height:68px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--line)}
|
|
10
|
-
.brand{display:flex;align-items:center;gap:4px;color:var(--green);font-size:21px;font-weight:700;text-decoration:none}
|
|
11
|
-
.brand-mark{display:grid;place-items:center;width:26px;height:26px;margin-right:4px;border-radius:7px;background:var(--green);color:var(--accent)}
|
|
12
|
-
.brand-dot{color:#99b878}
|
|
13
|
-
.brand small{margin-left:10px;font-size:9px;font-weight:600;letter-spacing:1px;color:#83907f}
|
|
14
|
-
.status{margin-left:auto;font-size:11px;color:#768270}
|
|
15
|
-
.login-section{min-height:calc(100dvh - 69px);display:grid;place-items:center}
|
|
16
|
-
.login-form{width:min(420px,100%);padding:32px;background:#fff;border:1px solid var(--line);border-radius:8px}
|
|
17
|
-
.eyebrow{font-size:9px;font-weight:700;letter-spacing:1.2px;color:#849379}
|
|
18
|
-
h1{margin:12px 0;font-size:25px;font-weight:600}
|
|
19
|
-
.login-form>p,.section-heading p{font-size:12px;line-height:1.8;color:#82907d}
|
|
20
|
-
label{display:grid;gap:6px;font-size:11px;color:#63745e}
|
|
21
|
-
input,textarea{width:100%;min-height:37px;padding:8px 10px;border:1px solid #dce4d6;border-radius:5px;background:#fff;color:#314332}
|
|
22
|
-
.login-form label{margin:20px 0 11px}
|
|
23
|
-
.primary{background:#204b36;color:#f2f6e8;border-color:#204b36}
|
|
24
|
-
.login-form>.primary{width:100%;margin-top:8px}
|
|
25
|
-
.error{min-height:18px;color:#b65e4a;font-size:11px}
|
|
26
|
-
.overview{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;padding:22px 0}
|
|
27
|
-
.overview>div{display:flex;align-items:center;justify-content:space-between;min-height:72px;padding:15px 17px;border:1px solid var(--line);border-radius:6px;background:#fff}
|
|
28
|
-
.overview span{font-size:11px;color:#82907d}
|
|
29
|
-
.overview strong{font-size:21px;font-weight:600}
|
|
30
|
-
.content-section{margin:0 0 24px;padding:20px;background:#fff;border:1px solid var(--line);border-radius:7px}
|
|
31
|
-
.section-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:17px}
|
|
32
|
-
.section-heading h2{margin:5px 0 0;font-size:18px;font-weight:600}
|
|
33
|
-
.user-filters{display:grid;grid-template-columns:auto minmax(180px,320px) auto minmax(130px,180px) 1fr;align-items:center;gap:9px;margin:0 0 14px}
|
|
34
|
-
.user-filters label{font-size:11px}
|
|
35
|
-
.user-filters input,.user-filters select{min-width:0;min-height:36px;padding:7px 9px;border:1px solid #dce4d6;border-radius:5px;background:#fff;color:#314332;font:inherit}
|
|
36
|
-
.user-filters span{text-align:right;color:#82907d;font-size:11px}
|
|
37
|
-
.create-user-form{display:grid;grid-template-columns:repeat(6,minmax(100px,1fr)) auto;align-items:end;gap:10px;margin-bottom:20px;padding:14px;background:#f7f9f4;border:1px solid #e9eee4;border-radius:5px}
|
|
38
|
-
.create-user-form label{min-width:0}
|
|
39
|
-
.create-user-form input{font-size:12px}
|
|
40
|
-
.users-list{display:grid;gap:9px}
|
|
41
|
-
.user-row{display:grid;grid-template-columns:minmax(180px,1.1fr) minmax(120px,.7fr) minmax(140px,.8fr) auto;gap:13px;align-items:center;padding:13px;border-bottom:1px solid #edf0e9}
|
|
42
|
-
.user-identity strong,.user-identity small{display:block;overflow-wrap:anywhere}
|
|
43
|
-
.user-identity small,.user-meta{margin-top:4px;color:#879280;font-size:10px}
|
|
44
|
-
.user-meta{line-height:1.8}
|
|
45
|
-
.badge{display:inline-flex;padding:4px 7px;border-radius:4px;background:#edf2e6;color:#65804f;font-size:9px}
|
|
46
|
-
.badge.suspended,.badge.revoked{background:#f5ece8;color:#a46250}
|
|
47
|
-
.badge.offline{background:#f2f0e7;color:#928257}
|
|
48
|
-
.user-actions{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end}
|
|
49
|
-
.user-settings{grid-column:1/-1;display:grid;grid-template-columns:minmax(140px,1.2fr) minmax(100px,.8fr) 100px 210px auto;align-items:end;gap:8px;padding:10px;background:#f7f9f5;border-radius:4px}
|
|
50
|
-
.user-settings label{min-width:0}
|
|
51
|
-
.user-settings input{font-size:11px}
|
|
52
|
-
.device-list{grid-column:1/-1;display:grid;gap:6px;padding-top:7px}
|
|
53
|
-
.device-row{display:flex;justify-content:space-between;gap:10px;padding:9px;background:#f7f9f5;border-radius:4px;font-size:10px}
|
|
54
|
-
.pagination{display:flex;align-items:center;justify-content:center;gap:14px;padding:16px 0 2px;color:#82907d;font-size:11px}
|
|
55
|
-
.pagination button{min-width:76px}
|
|
56
|
-
.audit-list{display:grid}
|
|
57
|
-
.audit-row{display:grid;grid-template-columns:180px 160px minmax(0,1fr);gap:12px;padding:10px 2px;border-bottom:1px solid #edf0e9;font-size:10px}
|
|
58
|
-
.audit-row time,.audit-row span{color:#85907d;overflow-wrap:anywhere}
|
|
59
|
-
dialog{width:min(520px,calc(100% - 28px));border:1px solid var(--line);border-radius:8px;padding:22px;background:#fff;color:#27392f}
|
|
60
|
-
dialog::backdrop{background:#10282077}
|
|
61
|
-
dialog h2{margin-top:0}
|
|
62
|
-
dialog label{margin:14px 0 7px}
|
|
63
|
-
dialog textarea{font-family:Consolas,monospace;overflow-wrap:anywhere;resize:vertical}
|
|
64
|
-
@media(max-width:1100px){.overview{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
|
65
|
-
@media(max-width:900px){.create-user-form{grid-template-columns:repeat(3,minmax(0,1fr))}.user-row{grid-template-columns:minmax(140px,1fr) minmax(100px,.7fr) auto}.user-meta.secondary{display:none}.user-settings{grid-template-columns:repeat(3,minmax(0,1fr))}.user-settings button{grid-column:1/-1}}
|
|
66
|
-
@media(max-width:600px){.admin-shell{width:calc(100% - 24px)}.admin-top{height:58px}.brand{font-size:18px}.brand small{font-size:8px}.overview{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;padding:12px 0}.overview>div{min-height:58px;padding:11px}.overview strong{font-size:18px}.content-section{padding:14px 11px}.user-filters{grid-template-columns:auto minmax(0,1fr);gap:8px}.user-filters input{grid-column:2}.user-filters select{grid-column:2}.user-filters span{grid-column:1/-1;text-align:left}.create-user-form{grid-template-columns:1fr 1fr}.create-user-form label:first-child,.create-user-form label:nth-child(2){grid-column:span 1}.create-user-form button{grid-column:1/-1}.user-row{grid-template-columns:1fr auto;gap:7px}.user-row>.user-meta{grid-column:1}.user-actions{grid-column:2;grid-row:1/3}.user-settings{grid-template-columns:1fr 100px}.user-settings label:first-child,.user-settings label:nth-child(2){grid-column:auto}.user-settings button{grid-column:1/-1}.device-row{align-items:flex-start;flex-direction:column}.audit-row{grid-template-columns:1fr auto}.audit-row span{grid-column:1/-1}}
|
|
67
|
-
@media(max-width:600px){.create-user-form>*{min-width:0}.create-user-form input[type=datetime-local]{min-width:0;width:100%;font-size:10px;padding-left:5px;padding-right:5px}.user-settings input[type=datetime-local]{min-width:0;width:100%;font-size:10px;padding-left:5px;padding-right:5px}}
|
package/public/admin.html
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#143a33"><title>Orbit Admin · 控制台</title><link rel="stylesheet" href="/admin.css"><script src="/admin.js" defer></script>
|
|
5
|
-
</head>
|
|
6
|
-
<body>
|
|
7
|
-
<main class="admin-shell">
|
|
8
|
-
<header class="admin-top"><a class="brand" href="/"><span class="brand-mark">o</span>orbit<span class="brand-dot">.</span><small>CONTROL PLANE</small></a><span id="admin-status" class="status">等待管理员登录</span><button id="admin-logout" type="button" hidden>退出</button></header>
|
|
9
|
-
<section id="admin-login" class="login-section"><form id="admin-login-form" class="login-form"><span class="eyebrow">OPERATOR ACCESS</span><h1>管理控制台</h1><p>使用服务器管理员密钥登录。该密钥只用于创建账号和设备,不会发给手机或电脑代理。</p><label for="admin-token">管理员密钥</label><input id="admin-token" type="password" autocomplete="current-password" required><button class="primary" type="submit">登录</button><p id="admin-login-error" class="error" role="alert"></p></form></section>
|
|
10
|
-
<section id="admin-app" hidden>
|
|
11
|
-
<section class="overview"><div><span>用户总数</span><strong id="overview-users">0</strong></div><div><span>有效用户</span><strong id="overview-active-users">0</strong></div><div><span>已停用</span><strong id="overview-suspended-users">0</strong></div><div><span>已到期</span><strong id="overview-expired-users">0</strong></div><div><span>有效电脑</span><strong id="overview-devices">0</strong></div><div><span>在线电脑</span><strong id="overview-online">0</strong></div></section>
|
|
12
|
-
<section class="content-section"><div class="section-heading"><div><span class="eyebrow">TENANTS</span><h2>用户与设备</h2></div><button id="refresh-admin" type="button">刷新</button></div>
|
|
13
|
-
<div class="user-filters"><label for="user-search">搜索用户</label><input id="user-search" type="search" maxlength="100" placeholder="用户名或显示名称"><label for="user-status">账户状态</label><select id="user-status"><option value="all">全部状态</option><option value="active">有效</option><option value="suspended">已停用</option><option value="expired">已到期</option></select><span id="user-count" role="status"></span></div>
|
|
14
|
-
<form id="create-user-form" class="create-user-form"><label>登录名<input name="username" minlength="3" maxlength="64" autocomplete="off" required></label><label>显示名称<input name="displayName" maxlength="120"></label><label>初始密码<input name="password" type="password" minlength="6" maxlength="256" autocomplete="new-password" placeholder="留空则自动生成"></label><label>套餐<input name="plan" value="standard" maxlength="40" required></label><label>设备上限<input name="maxDevices" type="number" min="1" max="50" value="2" required></label><label>账号到期<input name="expiresAt" type="datetime-local"></label><button class="primary" type="submit">创建账号</button></form>
|
|
15
|
-
<div id="admin-message" role="status" hidden></div><div id="users-list" class="users-list"></div>
|
|
16
|
-
<nav class="pagination" aria-label="用户列表分页"><button id="users-previous" type="button">上一页</button><span id="users-page">第 1 页</span><button id="users-next" type="button">下一页</button></nav>
|
|
17
|
-
</section>
|
|
18
|
-
<section class="content-section"><div class="section-heading"><div><span class="eyebrow">AUDIT</span><h2>管理事件</h2></div></div><div id="audit-list" class="audit-list"></div></section>
|
|
19
|
-
</section>
|
|
20
|
-
</main>
|
|
21
|
-
<dialog id="secret-dialog"><form method="dialog"><h2 id="secret-title">凭证已签发</h2><p id="secret-description"></p><label for="secret-value">仅显示一次</label><textarea id="secret-value" readonly rows="5"></textarea><button id="copy-secret" type="button">复制</button><button class="primary" value="close">完成</button></form></dialog>
|
|
22
|
-
</body>
|
|
23
|
-
</html>
|
package/public/admin.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
const select=selector=>document.querySelector(selector);
|
|
2
|
-
const escapeHtml=value=>String(value??'').replace(/[&<>"']/g,character=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
|
|
3
|
-
let users=[];
|
|
4
|
-
const userFilter={search:'',status:'all',page:1,pageSize:25};
|
|
5
|
-
let refreshTimer;
|
|
6
|
-
|
|
7
|
-
async function api(url,body,method='POST'){
|
|
8
|
-
const response=await fetch(url,body===undefined?{}:{
|
|
9
|
-
method,headers:{'Content-Type':'application/json'},body:JSON.stringify(body)
|
|
10
|
-
});
|
|
11
|
-
const result=await response.json();
|
|
12
|
-
if(!response.ok)throw new Error(result.error||'请求失败。');
|
|
13
|
-
return result;
|
|
14
|
-
}
|
|
15
|
-
function showSecret(title,description,value){
|
|
16
|
-
select('#secret-title').textContent=title;
|
|
17
|
-
select('#secret-description').textContent=description;
|
|
18
|
-
select('#secret-value').value=value;
|
|
19
|
-
select('#secret-dialog').showModal();
|
|
20
|
-
}
|
|
21
|
-
function flash(message){
|
|
22
|
-
const element=select('#admin-message');
|
|
23
|
-
element.hidden=false;element.textContent=message;
|
|
24
|
-
clearTimeout(flash.timer);flash.timer=setTimeout(()=>element.hidden=true,8000);
|
|
25
|
-
}
|
|
26
|
-
function localDateTime(timestamp){
|
|
27
|
-
if(!timestamp)return '';
|
|
28
|
-
const date=new Date(timestamp);
|
|
29
|
-
date.setSeconds(0,0);
|
|
30
|
-
date.setMinutes(date.getMinutes()-date.getTimezoneOffset());
|
|
31
|
-
return date.toISOString().slice(0,16);
|
|
32
|
-
}
|
|
33
|
-
function renderUsers(){
|
|
34
|
-
select('#users-list').innerHTML=users.map(user=>{
|
|
35
|
-
const devices=user.devices||[];
|
|
36
|
-
const expired=user.status==='active'&&user.expiresAt&&user.expiresAt<=Date.now();
|
|
37
|
-
const statusLabel=user.status==='suspended'?'已停用':expired?'已到期':'有效';
|
|
38
|
-
const statusClass=user.status==='suspended'||expired?'suspended':'';
|
|
39
|
-
const actions=[
|
|
40
|
-
`<button data-issue="${escapeHtml(user.id)}" type="button">签发电脑</button>`,
|
|
41
|
-
`<button data-reset="${escapeHtml(user.id)}" type="button">重置密码</button>`,
|
|
42
|
-
`<button data-status="${escapeHtml(user.id)}" data-next-status="${user.status==='active'?'suspended':'active'}" type="button">${user.status==='active'?'停用':'启用'}</button>`
|
|
43
|
-
].join('');
|
|
44
|
-
const rows=devices.map(device=>`<div class="device-row"><span><strong>${escapeHtml(device.name)}</strong> · ${device.online?'<span class="badge">在线</span>':'<span class="badge offline">离线</span>'}<br>设备 ID:${escapeHtml(device.id)} · 最近在线:${device.lastSeen?new Date(device.lastSeen).toLocaleString('zh-CN'):'从未'}</span>${device.status==='active'?`<button data-device="${escapeHtml(device.id)}" data-next-status="revoked" type="button">撤销凭证</button>`:'<span class="badge revoked">凭证已永久撤销</span>'}</div>`).join('');
|
|
45
|
-
return `<article class="user-row"><div class="user-identity"><strong>${escapeHtml(user.displayName)}</strong><small>${escapeHtml(user.username)} · <span class="badge ${statusClass}">${statusLabel}</span></small></div><div class="user-meta">套餐 ${escapeHtml(user.plan)}<br>设备 ${devices.filter(device=>device.status==='active').length}/${user.maxDevices}</div><div class="user-meta secondary">创建于 ${new Date(user.createdAt).toLocaleDateString('zh-CN')}${user.expiresAt?`<br>到期 ${new Date(user.expiresAt).toLocaleDateString('zh-CN')}`:'<br>不过期'}</div><div class="user-actions">${actions}</div><form class="user-settings" data-user-settings="${escapeHtml(user.id)}"><label>显示名称<input name="displayName" maxlength="120" value="${escapeHtml(user.displayName)}" required></label><label>套餐<input name="plan" maxlength="40" value="${escapeHtml(user.plan)}" required></label><label>设备上限<input name="maxDevices" type="number" min="1" max="50" value="${user.maxDevices}" required></label><label>到期时间<input name="expiresAt" type="datetime-local" value="${localDateTime(user.expiresAt)}"></label><button type="submit">保存设置</button></form><div class="device-list">${rows||'<div class="user-meta">尚未绑定电脑</div>'}</div></article>`;
|
|
46
|
-
}).join('')||(userFilter.search||userFilter.status!=='all'?'<p class="user-meta">没有符合条件的用户。</p>':'<p class="user-meta">尚未创建用户。</p>');
|
|
47
|
-
}
|
|
48
|
-
async function refresh(){
|
|
49
|
-
const {search,status,page,pageSize}=userFilter;
|
|
50
|
-
const query=new URLSearchParams({search,status,page:String(page),pageSize:String(pageSize)});
|
|
51
|
-
const [overview,userData,auditData]=await Promise.all([
|
|
52
|
-
api('/api/admin/overview'),api(`/api/admin/users?${query}`,undefined,'GET'),api('/api/admin/audit?limit=80',undefined,'GET')
|
|
53
|
-
]);
|
|
54
|
-
if(search!==userFilter.search||status!==userFilter.status||page!==userFilter.page)return;
|
|
55
|
-
users=userData.users;
|
|
56
|
-
userFilter.page=userData.page;
|
|
57
|
-
select('#overview-users').textContent=overview.users;
|
|
58
|
-
select('#overview-active-users').textContent=overview.activeUsers;
|
|
59
|
-
select('#overview-suspended-users').textContent=overview.suspendedUsers;
|
|
60
|
-
select('#overview-expired-users').textContent=overview.expiredUsers;
|
|
61
|
-
select('#overview-devices').textContent=overview.activeDevices;
|
|
62
|
-
select('#overview-online').textContent=overview.onlineDevices;
|
|
63
|
-
select('#admin-status').textContent=`Relay 在线 ${overview.onlineDevices} 台电脑`;
|
|
64
|
-
select('#user-count').textContent=`${userData.total} 个用户`;
|
|
65
|
-
const pageCount=Math.max(1,Math.ceil(userData.total/userData.pageSize));
|
|
66
|
-
select('#users-page').textContent=`第 ${userData.page} / ${pageCount} 页`;
|
|
67
|
-
select('#users-previous').disabled=userData.page<=1;
|
|
68
|
-
select('#users-next').disabled=userData.page>=pageCount;
|
|
69
|
-
renderUsers();
|
|
70
|
-
select('#audit-list').innerHTML=auditData.events.map(event=>`<div class="audit-row"><time>${new Date(event.createdAt).toLocaleString('zh-CN')}</time><strong>${escapeHtml(event.action)}</strong><span>${escapeHtml(event.actor)} · ${escapeHtml(event.targetId||'')}</span></div>`).join('')||'<p class="user-meta">暂无管理事件。</p>';
|
|
71
|
-
}
|
|
72
|
-
function scheduleUserRefresh(){
|
|
73
|
-
clearTimeout(refreshTimer);
|
|
74
|
-
refreshTimer=setTimeout(()=>refresh().catch(error=>flash(error.message)),250);
|
|
75
|
-
}
|
|
76
|
-
function enterApp(){
|
|
77
|
-
select('#admin-login').hidden=true;
|
|
78
|
-
select('#admin-app').hidden=false;
|
|
79
|
-
select('#admin-logout').hidden=false;
|
|
80
|
-
refresh().catch(error=>flash(error.message));
|
|
81
|
-
}
|
|
82
|
-
async function checkSession(){
|
|
83
|
-
try{await api('/api/admin/overview');enterApp()}catch{
|
|
84
|
-
select('#admin-login').hidden=false;
|
|
85
|
-
select('#admin-app').hidden=true;
|
|
86
|
-
select('#admin-logout').hidden=true;
|
|
87
|
-
select('#admin-status').textContent='需要管理员登录';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
select('#admin-login-form').addEventListener('submit',async event=>{
|
|
92
|
-
event.preventDefault();
|
|
93
|
-
const button=event.submitter;button.disabled=true;
|
|
94
|
-
select('#admin-login-error').textContent='';
|
|
95
|
-
try{await api('/api/admin/login',{token:select('#admin-token').value});select('#admin-token').value='';enterApp()}
|
|
96
|
-
catch(error){select('#admin-login-error').textContent=error.message}
|
|
97
|
-
finally{button.disabled=false}
|
|
98
|
-
});
|
|
99
|
-
select('#admin-logout').onclick=async()=>{
|
|
100
|
-
try{await api('/api/admin/logout',{});location.reload()}
|
|
101
|
-
catch(error){flash(error.message)}
|
|
102
|
-
};
|
|
103
|
-
select('#refresh-admin').onclick=()=>refresh().catch(error=>flash(error.message));
|
|
104
|
-
select('#user-search').addEventListener('input',event=>{
|
|
105
|
-
userFilter.search=event.currentTarget.value.trim();
|
|
106
|
-
userFilter.page=1;
|
|
107
|
-
scheduleUserRefresh();
|
|
108
|
-
});
|
|
109
|
-
select('#user-status').addEventListener('change',event=>{
|
|
110
|
-
userFilter.status=event.currentTarget.value;
|
|
111
|
-
userFilter.page=1;
|
|
112
|
-
refresh().catch(error=>flash(error.message));
|
|
113
|
-
});
|
|
114
|
-
select('#users-previous').onclick=()=>{
|
|
115
|
-
userFilter.page=Math.max(1,userFilter.page-1);
|
|
116
|
-
refresh().catch(error=>flash(error.message));
|
|
117
|
-
};
|
|
118
|
-
select('#users-next').onclick=()=>{
|
|
119
|
-
userFilter.page++;
|
|
120
|
-
refresh().catch(error=>flash(error.message));
|
|
121
|
-
};
|
|
122
|
-
select('#create-user-form').addEventListener('submit',async event=>{
|
|
123
|
-
event.preventDefault();
|
|
124
|
-
const form=event.currentTarget;
|
|
125
|
-
const button=event.submitter;button.disabled=true;
|
|
126
|
-
const values=Object.fromEntries(new FormData(form));
|
|
127
|
-
values.maxDevices=Number(values.maxDevices);
|
|
128
|
-
values.expiresAt=values.expiresAt?Date.parse(values.expiresAt):null;
|
|
129
|
-
if(!values.password)delete values.password;
|
|
130
|
-
try{
|
|
131
|
-
const result=await api('/api/admin/users',values);
|
|
132
|
-
showSecret('用户账号已创建',`请将登录名 ${result.user.username} 和此初始密码通过安全渠道交给用户。`,`${result.user.username}\n${result.initialPassword}`);
|
|
133
|
-
form.reset();form.elements.plan.value='standard';form.elements.maxDevices.value='2';
|
|
134
|
-
await refresh();
|
|
135
|
-
}catch(error){flash(error.message)}
|
|
136
|
-
finally{button.disabled=false}
|
|
137
|
-
});
|
|
138
|
-
document.addEventListener('click',async event=>{
|
|
139
|
-
const issue=event.target.closest('[data-issue]');
|
|
140
|
-
const reset=event.target.closest('[data-reset]');
|
|
141
|
-
const status=event.target.closest('[data-status]');
|
|
142
|
-
const device=event.target.closest('[data-device]');
|
|
143
|
-
try{
|
|
144
|
-
if(issue){
|
|
145
|
-
const name=window.prompt('电脑设备名称','我的电脑');
|
|
146
|
-
if(name===null)return;
|
|
147
|
-
const result=await api(`/api/admin/users/${encodeURIComponent(issue.dataset.issue)}/devices`,{name});
|
|
148
|
-
showSecret('电脑代理密钥',`只显示一次。把密钥写入电脑的 ORBIT_RELAY_AGENT_TOKEN。设备 ID:${result.device.id}`,result.agentToken);
|
|
149
|
-
await refresh();
|
|
150
|
-
}else if(reset){
|
|
151
|
-
const password=window.prompt('输入新密码(至少 6 个字符);留空则自动生成');
|
|
152
|
-
if(password===null)return;
|
|
153
|
-
const result=await api(`/api/admin/users/${encodeURIComponent(reset.dataset.reset)}/reset-password`,password?{password}:{});
|
|
154
|
-
showSecret('新密码已签发','旧用户会话已撤销,请通过安全渠道交付新密码。',result.initialPassword);
|
|
155
|
-
}else if(status){
|
|
156
|
-
await api(`/api/admin/users/${encodeURIComponent(status.dataset.status)}`,{status:status.dataset.nextStatus},'PATCH');
|
|
157
|
-
flash(status.dataset.nextStatus==='suspended'?'用户已停用,在线设备已断开。':'用户已重新启用。');
|
|
158
|
-
await refresh();
|
|
159
|
-
}else if(device){
|
|
160
|
-
await api(`/api/admin/devices/${encodeURIComponent(device.dataset.device)}`,{status:device.dataset.nextStatus},'PATCH');
|
|
161
|
-
flash(device.dataset.nextStatus==='revoked'?'设备凭证已撤销。':'设备已重新启用。');
|
|
162
|
-
await refresh();
|
|
163
|
-
}
|
|
164
|
-
}catch(error){flash(error.message)}
|
|
165
|
-
});
|
|
166
|
-
document.addEventListener('submit',async event=>{
|
|
167
|
-
const form=event.target.closest('[data-user-settings]');
|
|
168
|
-
if(!form)return;
|
|
169
|
-
event.preventDefault();
|
|
170
|
-
const button=event.submitter;button.disabled=true;
|
|
171
|
-
const values=Object.fromEntries(new FormData(form));
|
|
172
|
-
values.maxDevices=Number(values.maxDevices);
|
|
173
|
-
values.expiresAt=values.expiresAt?Date.parse(values.expiresAt):null;
|
|
174
|
-
try{
|
|
175
|
-
await api(`/api/admin/users/${encodeURIComponent(form.dataset.userSettings)}`,values,'PATCH');
|
|
176
|
-
flash('用户套餐与额度已更新。');
|
|
177
|
-
await refresh();
|
|
178
|
-
}catch(error){flash(error.message)}
|
|
179
|
-
finally{button.disabled=false}
|
|
180
|
-
});
|
|
181
|
-
select('#copy-secret').onclick=async()=>{
|
|
182
|
-
try{await navigator.clipboard.writeText(select('#secret-value').value);flash('凭证已复制。')}
|
|
183
|
-
catch{select('#secret-value').focus();select('#secret-value').select();flash('浏览器未授权剪贴板,请手动复制选中文本。')}
|
|
184
|
-
};
|
|
185
|
-
select('#secret-dialog').addEventListener('close',()=>{select('#secret-value').value=''});
|
|
186
|
-
checkSession();
|