nacho commited on
Commit
251d72f
·
1 Parent(s): a3f3a8e

feat: 面板登录密码 + 启动自动预登录账号

Browse files

- 新增登录遮罩层,需输入 admin key 才能进入控制台
- admin key 验证通过 /admin/verify 接口
- localStorage 持久化密钥,下次自动登录
- 启动时自动预登录所有账号(后台异步)
- 移除面板中多余的 admin key 输入框

Files changed (2) hide show
  1. main.py +26 -0
  2. static/index.html +75 -18
main.py CHANGED
@@ -268,6 +268,16 @@ async def list_accounts(admin_key: str = Header(...)):
268
  return {"accounts": accounts, "total": len(accounts)}
269
 
270
 
 
 
 
 
 
 
 
 
 
 
271
  @app.get("/")
272
  async def admin_panel():
273
  return RedirectResponse(url="/static/index.html")
@@ -285,6 +295,22 @@ async def startup():
285
 
286
  logger.info("Loaded %d accounts", len(config.accounts))
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  def main():
290
  import uvicorn
 
268
  return {"accounts": accounts, "total": len(accounts)}
269
 
270
 
271
+ @app.post("/admin/verify")
272
+ async def admin_verify(request: Request):
273
+ """Verify admin key for panel login."""
274
+ body = await request.json()
275
+ key = body.get("key", "")
276
+ if key != config.server.admin_key:
277
+ raise HTTPException(status_code=401, detail="Invalid admin key")
278
+ return {"ok": True}
279
+
280
+
281
  @app.get("/")
282
  async def admin_panel():
283
  return RedirectResponse(url="/static/index.html")
 
295
 
296
  logger.info("Loaded %d accounts", len(config.accounts))
297
 
298
+ # Pre-login all accounts in background so they show online immediately
299
+ asyncio.create_task(_prelogin_all())
300
+
301
+
302
+ async def _prelogin_all():
303
+ """Pre-login all accounts at startup for instant readiness."""
304
+ for email, account in manager.accounts.items():
305
+ try:
306
+ logger.info("Pre-logging in %s...", email)
307
+ await manager.get_or_create_browser_with_retry(
308
+ account, headless=config.browser.headless
309
+ )
310
+ logger.info("Pre-login OK: %s (muted=%s)", email, account.is_muted)
311
+ except Exception as e:
312
+ logger.error("Pre-login FAILED for %s: %s", email, e)
313
+
314
 
315
  def main():
316
  import uvicorn
static/index.html CHANGED
@@ -120,10 +120,37 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
120
  .empty{color:var(--text-muted);padding:24px 0;text-align:center;font-size:12px}
121
  .key-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
122
  @media(max-width:600px){.key-row{grid-template-columns:1fr}}
 
 
 
 
 
 
 
 
 
 
 
 
123
  </style>
124
  </head>
125
  <body>
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  <div class="topbar">
128
  <span class="logo">▸ DS2API</span>
129
  <span class="badge-mode">浏览器模式</span>
@@ -157,15 +184,9 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
157
  <span class="hint">/v1/chat/completions</span>
158
  </div>
159
  <div class="card-body">
160
- <div class="key-row">
161
- <div class="form-group">
162
- <label>API Key</label>
163
- <input type="password" id="apiKey" placeholder="sk-xxx">
164
- </div>
165
- <div class="form-group">
166
- <label>Admin Key</label>
167
- <input type="password" id="adminKey" placeholder="admin key">
168
- </div>
169
  </div>
170
  <div class="row" style="gap:10px;margin-bottom:14px">
171
  <div class="form-group" style="flex:1;margin-bottom:0">
@@ -229,6 +250,7 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
229
 
230
  </div>
231
  </div>
 
232
 
233
  <script>
234
  const H=location.origin;
@@ -237,16 +259,48 @@ const LS={
237
  set(k,v){try{localStorage.setItem('ds2_'+k,v)}catch(e){}}
238
  };
239
 
240
- // Restore saved keys
241
- document.getElementById('apiKey').value=LS.get('apiKey','');
242
- document.getElementById('adminKey').value=LS.get('adminKey','');
243
 
244
- // Auto-save keys on change
245
- document.getElementById('apiKey').addEventListener('input',e=>LS.set('apiKey',e.target.value));
246
- document.getElementById('adminKey').addEventListener('input',e=>LS.set('adminKey',e.target.value));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  function getApiKey(){return document.getElementById('apiKey').value.trim()||'sk-default'}
249
- function getAdminKey(){return document.getElementById('adminKey').value.trim()||'admin'}
 
 
 
 
250
 
251
  function toast(m,ok){
252
  const e=document.createElement('div');
@@ -396,8 +450,11 @@ async function loadAll(){await loadStats();await loadAccounts()}
396
  // Ctrl+Enter to send
397
  document.getElementById('prompt').addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')sendMsg()});
398
 
399
- loadAll();
400
- setInterval(loadAll,12000);
 
 
 
401
  </script>
402
  </body>
403
  </html>
 
120
  .empty{color:var(--text-muted);padding:24px 0;text-align:center;font-size:12px}
121
  .key-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
122
  @media(max-width:600px){.key-row{grid-template-columns:1fr}}
123
+
124
+ /* ── login overlay ── */
125
+ .login-overlay{position:fixed;inset:0;z-index:200;background:var(--bg);display:flex;align-items:center;justify-content:center;transition:opacity .4s}
126
+ .login-overlay.hidden{opacity:0;pointer-events:none}
127
+ .login-box{background:var(--surface);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:40px;width:100%;max-width:380px;text-align:center;animation:fadeUp .5s ease-out}
128
+ .login-box .logo-big{font-size:24px;font-weight:800;background:linear-gradient(135deg,var(--accent),hsl(280,80%,65%));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
129
+ .login-box .sub{color:var(--text-dim);font-size:12px;margin-bottom:28px}
130
+ .login-box .form-group{text-align:left;margin-bottom:16px}
131
+ .login-box .btn-primary{width:100%;padding:12px;font-size:14px}
132
+ .login-box .err-msg{color:var(--red);font-size:12px;margin-top:10px;min-height:18px}
133
+ .app-wrap{transition:filter .3s}
134
+ .app-wrap.blur{filter:blur(8px);pointer-events:none}
135
  </style>
136
  </head>
137
  <body>
138
 
139
+ <!-- Login Overlay -->
140
+ <div class="login-overlay" id="loginOverlay">
141
+ <div class="login-box">
142
+ <div class="logo-big">▸ DS2API</div>
143
+ <div class="sub">输入管理密钥进入控制台</div>
144
+ <div class="form-group">
145
+ <label>Admin Key</label>
146
+ <input type="password" id="loginKey" placeholder="请输入管理密钥" autofocus>
147
+ </div>
148
+ <button class="btn btn-primary" onclick="doLogin()" id="loginBtn">进入控制台</button>
149
+ <div class="err-msg" id="loginErr"></div>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="app-wrap" id="appWrap">
154
  <div class="topbar">
155
  <span class="logo">▸ DS2API</span>
156
  <span class="badge-mode">浏览器模式</span>
 
184
  <span class="hint">/v1/chat/completions</span>
185
  </div>
186
  <div class="card-body">
187
+ <div class="form-group">
188
+ <label>API Key</label>
189
+ <input type="password" id="apiKey" placeholder="sk-xxx">
 
 
 
 
 
 
190
  </div>
191
  <div class="row" style="gap:10px;margin-bottom:14px">
192
  <div class="form-group" style="flex:1;margin-bottom:0">
 
250
 
251
  </div>
252
  </div>
253
+ </div><!-- /app-wrap -->
254
 
255
  <script>
256
  const H=location.origin;
 
259
  set(k,v){try{localStorage.setItem('ds2_'+k,v)}catch(e){}}
260
  };
261
 
262
+ let _adminKey='';
 
 
263
 
264
+ // ── Auth Gate ──
265
+ async function doLogin(){
266
+ const key=document.getElementById('loginKey').value.trim();
267
+ const err=document.getElementById('loginErr');
268
+ const btn=document.getElementById('loginBtn');
269
+ if(!key){err.textContent='请输入密钥';return}
270
+ btn.disabled=true;btn.textContent='验证中…';err.textContent='';
271
+ try{
272
+ const r=await fetch(H+'/admin/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})});
273
+ if(!r.ok)throw new Error('密钥错误');
274
+ _adminKey=key;
275
+ LS.set('adminKey',key);
276
+ document.getElementById('loginOverlay').classList.add('hidden');
277
+ document.getElementById('appWrap').classList.remove('blur');
278
+ initApp();
279
+ }catch(e){err.textContent=e.message}
280
+ btn.disabled=false;btn.textContent='进入控制台';
281
+ }
282
+
283
+ document.getElementById('loginKey').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
284
+
285
+ // Auto-login if key saved
286
+ (async()=>{
287
+ const saved=LS.get('adminKey','');
288
+ if(saved){
289
+ document.getElementById('loginKey').value=saved;
290
+ try{
291
+ const r=await fetch(H+'/admin/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:saved})});
292
+ if(r.ok){_adminKey=saved;document.getElementById('loginOverlay').classList.add('hidden');document.getElementById('appWrap').classList.remove('blur');initApp();return}
293
+ }catch(e){}
294
+ }
295
+ document.getElementById('appWrap').classList.add('blur');
296
+ })();
297
 
298
  function getApiKey(){return document.getElementById('apiKey').value.trim()||'sk-default'}
299
+ function getAdminKey(){return _adminKey||'admin'}
300
+
301
+ // Restore API key
302
+ document.getElementById('apiKey').value=LS.get('apiKey','');
303
+ document.getElementById('apiKey').addEventListener('input',e=>LS.set('apiKey',e.target.value));
304
 
305
  function toast(m,ok){
306
  const e=document.createElement('div');
 
450
  // Ctrl+Enter to send
451
  document.getElementById('prompt').addEventListener('keydown',e=>{if(e.ctrlKey&&e.key==='Enter')sendMsg()});
452
 
453
+ let _pollTimer=null;
454
+ function initApp(){
455
+ loadAll();
456
+ if(!_pollTimer)_pollTimer=setInterval(loadAll,12000);
457
+ }
458
  </script>
459
  </body>
460
  </html>