orbit-codex-bridge 0.0.0-stage → 0.1.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.
@@ -0,0 +1,186 @@
1
+ const select=selector=>document.querySelector(selector);
2
+ const escapeHtml=value=>String(value??'').replace(/[&<>"']/g,character=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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();