Просмотр исходного кода

refactor(auth): switch to password-based auth

CassiopeiaCode 2 месяцев назад
Родитель
Сommit
be387ee8ca

+ 4 - 1
account-feeder/.env.example

@@ -10,4 +10,7 @@ API_SERVER=http://localhost:8000
 
 # HTTP代理设置(留空不使用代理)
 # 例如:HTTP_PROXY="http://127.0.0.1:7890"
-HTTP_PROXY=""
+HTTP_PROXY=""
+
+# 主服务密码
+API_SERVER_PASSWORD=

+ 3 - 0
account-feeder/README.md

@@ -70,6 +70,9 @@ API_SERVER=http://localhost:8000
 
 # HTTP代理设置(可选)
 HTTP_PROXY=""
+
+# 主服务管理员密码(如果主服务配置了 ADMIN_PASSWORD)
+API_SERVER_PASSWORD=
 ```
 
 **重要:** 确保 `API_SERVER` 配置正确,指向主服务地址!

+ 16 - 3
account-feeder/app.py

@@ -23,6 +23,7 @@ load_dotenv()
 # 配置
 PORT = int(os.getenv("FEEDER_PORT", "8001"))
 API_SERVER = os.getenv("API_SERVER", "http://localhost:8000")
+API_SERVER_PASSWORD = os.getenv("API_SERVER_PASSWORD")
 
 # OIDC 端点
 OIDC_BASE = "https://oidc.us-east-1.amazonaws.com"
@@ -235,11 +236,15 @@ async def auth_claim(auth_id: str):
             "enabled": False,
         }
 
+        headers = {"content-type": "application/json"}
+        if API_SERVER_PASSWORD:
+            headers["Authorization"] = f"Bearer {API_SERVER_PASSWORD}"
+
         async with httpx.AsyncClient(timeout=30.0) as client:
             r = await client.post(
                 f"{API_SERVER}/v2/accounts",
                 json=account_data,
-                headers={"content-type": "application/json"},
+                headers=headers,
             )
             r.raise_for_status()
             account = r.json()
@@ -275,11 +280,15 @@ async def create_account(account: AccountCreate):
         # 包装成列表以调用新的批量接口
         batch_request = {"accounts": [account_data]}
 
+        headers = {"content-type": "application/json"}
+        if API_SERVER_PASSWORD:
+            headers["Authorization"] = f"Bearer {API_SERVER_PASSWORD}"
+
         async with httpx.AsyncClient(timeout=30.0) as client:
             r = await client.post(
                 f"{API_SERVER}/v2/accounts/feed",
                 json=batch_request,
-                headers={"content-type": "application/json"},
+                headers=headers,
             )
             r.raise_for_status()
             return r.json()
@@ -297,11 +306,15 @@ async def create_account(account: AccountCreate):
 async def batch_create_accounts(request: BatchCreateRequest):
     """批量创建账号(调用主服务统一feed接口)"""
     try:
+        headers = {"content-type": "application/json"}
+        if API_SERVER_PASSWORD:
+            headers["Authorization"] = f"Bearer {API_SERVER_PASSWORD}"
+
         async with httpx.AsyncClient(timeout=60.0) as client:
             r = await client.post(
                 f"{API_SERVER}/v2/accounts/feed",
                 json={"accounts": request.accounts},
-                headers={"content-type": "application/json"},
+                headers=headers,
             )
             r.raise_for_status()
             return r.json()

+ 1 - 8
account-feeder/docker-compose.yml

@@ -16,11 +16,4 @@ services:
       test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
       interval: 30s
       timeout: 10s
-      retries: 3
-    networks:
-      - q2api-network
-
-networks:
-  q2api-network:
-    external: true
-    name: v2_default
+      retries: 3

+ 21 - 40
app.py

@@ -259,10 +259,6 @@ CONSOLE_ENABLED: bool = _is_console_enabled()
 
 # Admin authentication configuration
 ADMIN_PASSWORD: str = os.getenv("ADMIN_PASSWORD", "admin")
-SESSION_EXPIRE_DAYS: int = 30
-
-# Admin session storage: {token: expiration_datetime}
-ADMIN_SESSIONS: Dict[str, datetime] = {}
 
 def _extract_bearer(token_header: Optional[str]) -> Optional[str]:
     if not token_header:
@@ -478,27 +474,20 @@ async def require_account(
     key = _extract_bearer(authorization) if authorization else x_api_key
     return await resolve_account_for_key(key)
 
-def verify_admin_session(authorization: Optional[str] = Header(None)) -> bool:
-    """Verify admin session token for console access"""
+def verify_admin_password(authorization: Optional[str] = Header(None)) -> bool:
+    """Verify admin password for console access"""
     if not authorization or not authorization.startswith("Bearer "):
         raise HTTPException(
             status_code=401,
             detail={"error": "Unauthorized access", "code": "UNAUTHORIZED"}
         )
 
-    token = authorization[7:]  # Remove "Bearer " prefix
-
-    if token not in ADMIN_SESSIONS:
-        raise HTTPException(
-            status_code=401,
-            detail={"error": "Invalid session", "code": "SESSION_INVALID"}
-        )
+    password = authorization[7:]  # Remove "Bearer " prefix
 
-    if datetime.now() > ADMIN_SESSIONS[token]:
-        del ADMIN_SESSIONS[token]
+    if password != ADMIN_PASSWORD:
         raise HTTPException(
             status_code=401,
-            detail={"error": "Session expired", "code": "SESSION_EXPIRED"}
+            detail={"error": "Invalid password", "code": "INVALID_PASSWORD"}
         )
 
     return True
@@ -924,7 +913,6 @@ class AdminLoginRequest(BaseModel):
 
 class AdminLoginResponse(BaseModel):
     success: bool
-    token: Optional[str] = None
     message: str
 
 async def _create_account_from_tokens(
@@ -969,24 +957,17 @@ if CONSOLE_ENABLED:
     @app.post("/api/login", response_model=AdminLoginResponse)
     async def admin_login(request: AdminLoginRequest) -> AdminLoginResponse:
         """Admin login endpoint - password only"""
-        if request.password != ADMIN_PASSWORD:
+        if request.password == ADMIN_PASSWORD:
+            return AdminLoginResponse(
+                success=True,
+                message="Login successful"
+            )
+        else:
             return AdminLoginResponse(
                 success=False,
                 message="Invalid password"
             )
 
-        # Generate session token
-        session_token = secrets.token_urlsafe(32)
-
-        # Store session with expiration
-        ADMIN_SESSIONS[session_token] = datetime.now() + timedelta(days=SESSION_EXPIRE_DAYS)
-
-        return AdminLoginResponse(
-            success=True,
-            token=session_token,
-            message="Login successful"
-        )
-
     @app.get("/login", response_class=FileResponse)
     def login_page():
         """Serve the login page"""
@@ -1000,7 +981,7 @@ if CONSOLE_ENABLED:
     # ------------------------------------------------------------------------------
 
     @app.post("/v2/auth/start")
-    async def auth_start(body: AuthStartBody, _: bool = Depends(verify_admin_session)):
+    async def auth_start(body: AuthStartBody, _: bool = Depends(verify_admin_password)):
         """
         Start device authorization and return verification URL for user login.
         Session lifetime capped at 5 minutes on claim.
@@ -1037,7 +1018,7 @@ if CONSOLE_ENABLED:
         }
 
     @app.get("/v2/auth/status/{auth_id}")
-    async def auth_status(auth_id: str, _: bool = Depends(verify_admin_session)):
+    async def auth_status(auth_id: str, _: bool = Depends(verify_admin_password)):
         sess = AUTH_SESSIONS.get(auth_id)
         if not sess:
             raise HTTPException(status_code=404, detail="Auth session not found")
@@ -1052,7 +1033,7 @@ if CONSOLE_ENABLED:
         }
 
     @app.post("/v2/auth/claim/{auth_id}")
-    async def auth_claim(auth_id: str, _: bool = Depends(verify_admin_session)):
+    async def auth_claim(auth_id: str, _: bool = Depends(verify_admin_password)):
         """
         Block up to 5 minutes to exchange the device code for tokens after user completed login.
         On success, creates an enabled account and returns it.
@@ -1107,7 +1088,7 @@ if CONSOLE_ENABLED:
     # ------------------------------------------------------------------------------
 
     @app.post("/v2/accounts")
-    async def create_account(body: AccountCreate, _: bool = Depends(verify_admin_session)):
+    async def create_account(body: AccountCreate, _: bool = Depends(verify_admin_password)):
         now = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
         acc_id = str(uuid.uuid4())
         other_str = json.dumps(body.other, ensure_ascii=False) if body.other is not None else None
@@ -1156,7 +1137,7 @@ if CONSOLE_ENABLED:
                 traceback.print_exc()
 
     @app.post("/v2/accounts/feed")
-    async def create_accounts_feed(request: BatchAccountCreate, _: bool = Depends(verify_admin_session)):
+    async def create_accounts_feed(request: BatchAccountCreate, _: bool = Depends(verify_admin_password)):
         """
         统一的投喂接口,接收账号列表,立即存入并后台异步验证。
         """
@@ -1202,23 +1183,23 @@ if CONSOLE_ENABLED:
         }
 
     @app.get("/v2/accounts")
-    async def list_accounts(_: bool = Depends(verify_admin_session)):
+    async def list_accounts(_: bool = Depends(verify_admin_password)):
         rows = await _db.fetchall("SELECT * FROM accounts ORDER BY created_at DESC")
         return [_row_to_dict(r) for r in rows]
 
     @app.get("/v2/accounts/{account_id}")
-    async def get_account_detail(account_id: str, _: bool = Depends(verify_admin_session)):
+    async def get_account_detail(account_id: str, _: bool = Depends(verify_admin_password)):
         return await get_account(account_id)
 
     @app.delete("/v2/accounts/{account_id}")
-    async def delete_account(account_id: str, _: bool = Depends(verify_admin_session)):
+    async def delete_account(account_id: str, _: bool = Depends(verify_admin_password)):
         rowcount = await _db.execute("DELETE FROM accounts WHERE id=?", (account_id,))
         if rowcount == 0:
             raise HTTPException(status_code=404, detail="Account not found")
         return {"deleted": account_id}
 
     @app.patch("/v2/accounts/{account_id}")
-    async def update_account(account_id: str, body: AccountUpdate, _: bool = Depends(verify_admin_session)):
+    async def update_account(account_id: str, body: AccountUpdate, _: bool = Depends(verify_admin_password)):
         now = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
         fields = []
         values: List[Any] = []
@@ -1251,7 +1232,7 @@ if CONSOLE_ENABLED:
         return _row_to_dict(row)
 
     @app.post("/v2/accounts/{account_id}/refresh")
-    async def manual_refresh(account_id: str, _: bool = Depends(verify_admin_session)):
+    async def manual_refresh(account_id: str, _: bool = Depends(verify_admin_password)):
         return await refresh_access_token_in_db(account_id)
 
     # ------------------------------------------------------------------------------

+ 8 - 8
frontend/index.html

@@ -315,14 +315,14 @@ function api(path){
 }
 
 // Authentication helpers
-function getAuthToken() {
-  return localStorage.getItem('adminToken');
+function getAuthPassword() {
+  return localStorage.getItem('adminPassword');
 }
 
 function getAuthHeaders() {
-  const token = getAuthToken();
-  if (!token) return {};
-  return { 'Authorization': `Bearer ${token}` };
+  const password = getAuthPassword();
+  if (!password) return {};
+  return { 'Authorization': `Bearer ${password}` };
 }
 
 // Authenticated fetch wrapper
@@ -331,7 +331,7 @@ async function authFetch(url, options = {}) {
   const response = await fetch(url, { ...options, headers });
 
   if (response.status === 401) {
-    localStorage.removeItem('adminToken');
+    localStorage.removeItem('adminPassword');
     window.location.href = '/login';
     throw new Error('Unauthorized');
   }
@@ -790,8 +790,8 @@ async function send() {
 
 window.addEventListener('DOMContentLoaded', () => {
   // Check authentication first
-  const token = getAuthToken();
-  if (!token) {
+  const password = getAuthPassword();
+  if (!password) {
     window.location.href = '/login';
     return;
   }

+ 1 - 9
frontend/login.html

@@ -49,7 +49,7 @@
                 });
                 const d=await r.json();
                 if(d.success){
-                    localStorage.setItem('adminToken',d.token);
+                    localStorage.setItem('adminPassword',fd.get('password'));
                     location.href='/';
                 }else{
                     showToast(d.message||'Login failed','error');
@@ -74,14 +74,6 @@
             },2000);
         }
 
-        window.addEventListener('DOMContentLoaded',()=>{
-            const t=localStorage.getItem('adminToken');
-            if(t){
-                fetch('/v2/accounts',{headers:{Authorization:`Bearer ${t}`}})
-                    .then(r=>{if(r.ok)location.href='/'})
-                    .catch(()=>{});
-            }
-        });
     </script>
 </body>
 </html>