Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async def get_users(request: Request):
status_code=500,
content={
"status": False,
"message": str(e)
"message": "Internal server error"
}
)

Expand Down Expand Up @@ -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"
}
)

Expand Down Expand Up @@ -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"
}
)

Expand Down Expand Up @@ -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"
}
)
18 changes: 11 additions & 7 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import json
import datetime
from discord import Embed
from ..core import DiscordAuthService, config, limiter, datamanager, pterclient, dchook, AuthClient
Expand Down Expand Up @@ -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")
Expand All @@ -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
21 changes: 8 additions & 13 deletions app/api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
)

Expand All @@ -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"
}
)

Expand Down Expand Up @@ -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"
}
)

Expand All @@ -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"
}
)

Expand Down Expand Up @@ -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"
}
)

4 changes: 2 additions & 2 deletions app/api/shop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 4 additions & 2 deletions app/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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):
Expand Down
104 changes: 79 additions & 25 deletions app/core/security.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,102 @@
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 = 10800 # 3 hours

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"
)

return user
return user
Loading