From f6924ba091f88c87170249e211ce762364eac0f2 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 11 Jul 2026 17:26:30 +0000 Subject: [PATCH 1/2] fix: harden auth and error handling --- app/api/admin.py | 8 ++-- app/api/auth.py | 18 +++++--- app/api/dashboard.py | 21 ++++----- app/api/shop.py | 4 +- app/core/client.py | 6 ++- app/core/security.py | 102 +++++++++++++++++++++++++++++++++---------- 6 files changed, 107 insertions(+), 52 deletions(-) diff --git a/app/api/admin.py b/app/api/admin.py index e293c43..502f0cf 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -32,7 +32,7 @@ async def get_users(request: Request): status_code=500, content={ "status": False, - "message": str(e) + "message": "Internal server error" } ) @@ -143,7 +143,7 @@ async def update_user(request: Request, discord_id: str, admin_data: dict = Depe status_code=500, content={ "status": False, - "message": str(e) + "message": "Internal server error" } ) @@ -235,7 +235,7 @@ async def update_user_password(request: Request, discord_id: str, admin_data: di status_code=500, content={ "status": False, - "message": str(e) + "message": "Internal server error" } ) @@ -328,6 +328,6 @@ async def delete_user(request: Request, discord_id: str, admin_data: dict = Depe status_code=500, content={ "status": False, - "message": str(e) + "message": "Internal server error" } ) \ No newline at end of file diff --git a/app/api/auth.py b/app/api/auth.py index 0baec94..5e3089c 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -1,5 +1,4 @@ import logging -import json import datetime from discord import Embed from ..core import DiscordAuthService, config, limiter, datamanager, pterclient, dchook, AuthClient @@ -107,20 +106,24 @@ async def callback(request: Request, code: str = None): """, ) await dchook.post(embeds=[embed], username="[Auth]") - encrypted = AuthClient.encrypt_session(user_data) + session_id = AuthClient.create_session(user_data) redirect_response = RedirectResponse(url="/dashboard", status_code=status.HTTP_303_SEE_OTHER) redirect_response.set_cookie( - key="session_user", - value=encrypted, + key="session_user", + value=session_id, httponly=True, + secure=not config.get_config("app.debug", False), samesite="lax", - max_age=10800 + max_age=10800, + path="/", ) return redirect_response + except HTTPException: + raise except Exception as e: logging.error(f"Callback error: {e}") - raise HTTPException(status_code=400, detail=str(e)) + raise HTTPException(status_code=400, detail="Authentication failed") @router.post("/logout") @limiter.limit("2/second;15/minute") @@ -129,5 +132,6 @@ async def logout(request: Request): status_code=status.HTTP_200_OK, content={"status": "success", "message": "logout successfully"} ) - response.delete_cookie(key="session_user") + AuthClient.delete_session(request) + response.delete_cookie(key="session_user", path="/") return response \ No newline at end of file diff --git a/app/api/dashboard.py b/app/api/dashboard.py index e941ace..79aabe8 100644 --- a/app/api/dashboard.py +++ b/app/api/dashboard.py @@ -42,8 +42,7 @@ async def get_user_servers(request: Request, user: dict = Depends(DiscordAuthSer status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "status": "error", - "message": "Failed to get user's servers", - "detail": str(e) + "message": "Failed to get user's servers" } ) @@ -52,13 +51,12 @@ async def get_user_servers(request: Request, user: dict = Depends(DiscordAuthSer async def create_server_route(request: Request, user: dict = Depends(DiscordAuthService.get_current_user)): try: server_data = await request.json() - except Exception as e: + except Exception: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={ "status": "error", - "message": "Request json error", - "detail": str(e) + "message": "Request json error" } ) @@ -233,8 +231,7 @@ async def create_server_route(request: Request, user: dict = Depends(DiscordAuth status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "status": "error", - "message": "Panel rejected request or internal automation failed", - "detail": str(e) + "message": "Panel rejected request or internal automation failed" } ) @@ -246,13 +243,12 @@ async def delete_server_route(request: Request, user: dict = Depends(DiscordAuth response = await request.json() server_id = response.get("server_id") force = response.get("force", False) - except Exception as e: + except Exception: return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "status": "error", - "message": "Request json error", - "detail": str(e) + "message": "Panel rejected request or internal automation failed" } ) @@ -315,8 +311,7 @@ async def delete_server_route(request: Request, user: dict = Depends(DiscordAuth status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "status": "error", - "message": "Failed to delete server", - "detail": str(e) + "message": "Failed to delete server" } ) \ No newline at end of file diff --git a/app/api/shop.py b/app/api/shop.py index 2f47b73..ab47791 100644 --- a/app/api/shop.py +++ b/app/api/shop.py @@ -15,10 +15,10 @@ async def buy_resources(request: Request): req_data = await request.json() item_type = req_data.get("item_type") raw_amount = req_data.get("amount", 1) - except Exception as e: + except Exception: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"status": "error", "message": "Request JSON error", "detail": str(e)} + content={"status": "error", "message": "Request JSON error"} ) if not config.get_config(f"price.{item_type}", None): diff --git a/app/core/client.py b/app/core/client.py index 4362cdf..3e35ff3 100644 --- a/app/core/client.py +++ b/app/core/client.py @@ -273,7 +273,8 @@ async def create_server( return new_server_result except Exception as e: - raise Exception(f"Direct API create error: {str(e)}") + logging.error(f"Direct API create error: {e}") + raise Exception("Direct API create error") async def delete_server(self, server_id: int, user_id: int = None, force: bool = True) -> dict: endpoint = f"servers/{server_id}/force" if force else f"servers/{server_id}" @@ -331,7 +332,8 @@ async def update_password(self, user_id: int, password: str): return response except Exception as e: - raise Exception(f"Password update failed: {str(e)}") + logging.error(f"Password update failed: {e}") + raise Exception("Password update failed") async def delete_user(self, user_id: int): diff --git a/app/core/security.py b/app/core/security.py index 6b11f49..e6538ef 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -1,45 +1,99 @@ -import json -import secrets +import datetime import logging -from fastapi import Request, HTTPException, Depends -from itsdangerous import Signer, BadSignature +import secrets +from datetime import timezone, timedelta + +from fastapi import Depends, HTTPException, Request +from pymongo import MongoClient + from .database import config, datamanager -if config.get_config("app.debug", False): - SECRET_KEY = config.get_config("app.key", "sfjsdghfdkjdlk") - # logging.info("Enable default key for debug mode.") -else: - SECRET_KEY = secrets.token_hex(64) - # logging.info("Generated secret key.") +SESSION_COOKIE_NAME = "session_user" +SESSION_TTL_SECONDS = 60 * 60 * 3 -signer = Signer(SECRET_KEY) -class AuthClient: +class _SessionStore: + def __init__(self): + uri = config.get_config("mongo_uri") + db_name = config.get_config("database.db") + self.client = MongoClient(uri) + self.collection = self.client[db_name]["sessions"] + try: + self.collection.create_index("session_id", unique=True) + self.collection.create_index("expires_at", expireAfterSeconds=0) + except Exception as exc: + logging.warning(f"Session index setup skipped: {exc}") + + def create(self, user_dict: dict) -> str: + session_id = secrets.token_urlsafe(32) + now = datetime.datetime.now(timezone.utc) + payload = { + "session_id": session_id, + "user": { + "id": str(user_dict.get("id")), + "username": user_dict.get("username"), + "email": user_dict.get("email"), + "avatar": user_dict.get("avatar"), + "global_name": user_dict.get("global_name"), + }, + "created_at": now, + "expires_at": now + timedelta(seconds=SESSION_TTL_SECONDS), + } + self.collection.insert_one(payload) + return session_id + + def get(self, session_id: str) -> dict | None: + session = self.collection.find_one({"session_id": session_id}) + if not session: + return None + + expires_at = session.get("expires_at") + if expires_at and expires_at < datetime.datetime.now(timezone.utc): + self.collection.delete_one({"session_id": session_id}) + return None + + user = session.get("user") or {} + if not user.get("id"): + return None + return user + def delete(self, session_id: str) -> None: + self.collection.delete_one({"session_id": session_id}) + + +_session_store = _SessionStore() + + +class AuthClient: @staticmethod - def encrypt_session(user_dict: dict) -> str: - json_str = json.dumps(user_dict) - return signer.sign(json_str.encode('utf-8')).decode('utf-8') + def create_session(user_dict: dict) -> str: + return _session_store.create(user_dict) @staticmethod def get_current_user(request: Request) -> dict: - encrypted_cookie = request.cookies.get("session_user") + session_id = request.cookies.get(SESSION_COOKIE_NAME) - if not encrypted_cookie: + if not session_id: raise HTTPException(status_code=401, detail="Not authenticated") - try: - unsigned_bytes = signer.unsign(encrypted_cookie.encode('utf-8')) - return json.loads(unsigned_bytes.decode('utf-8')) - except BadSignature: - raise HTTPException(status_code=401, detail="Invalid token or tampered cookie") + user = _session_store.get(session_id) + if not user: + raise HTTPException(status_code=401, detail="Invalid or expired session") + + return user + + @staticmethod + def delete_session(request: Request) -> None: + session_id = request.cookies.get(SESSION_COOKIE_NAME) + if session_id: + _session_store.delete(session_id) @staticmethod async def is_user_admin( - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(AuthClient.get_current_user) ) -> dict | None: user = datamanager.find_one({"discord_id": current_user.get("id")}) - if not user.get("admin", False): + if not user or not user.get("admin", False): raise HTTPException( status_code=403, detail="Forbidden to control" From dcb048f541532f34e4fd9d4a9d2b6052ebb8e9b6 Mon Sep 17 00:00:00 2001 From: command_cat <131455122+littlecommandcat@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:42:06 +0800 Subject: [PATCH 2/2] Update session to 3 hours --- app/core/security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/security.py b/app/core/security.py index e6538ef..4514137 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -9,7 +9,7 @@ from .database import config, datamanager SESSION_COOKIE_NAME = "session_user" -SESSION_TTL_SECONDS = 60 * 60 * 3 +SESSION_TTL_SECONDS = 10800 # 3 hours class _SessionStore: @@ -99,4 +99,4 @@ async def is_user_admin( detail="Forbidden to control" ) - return user \ No newline at end of file + return user