User Log and Persistent Faction Information

This commit is contained in:
2026-01-27 14:48:46 -05:00
parent 4ae3a9eb17
commit 4850c16b87
39 changed files with 782 additions and 71 deletions

Binary file not shown.

52
routers/activity.py Normal file
View File

@@ -0,0 +1,52 @@
"""Activity log endpoints."""
from fastapi import APIRouter, Request
from pydantic import BaseModel
from utils.auth import get_current_user
from services.activity_log import activity_logger
router = APIRouter(prefix="/api", tags=["activity"])
class LogActionRequest(BaseModel):
action: str
details: str = ""
@router.get("/active_users")
async def get_active_users(request: Request):
"""Get list of currently active users"""
# Update current user's activity
try:
user_info = get_current_user(request)
username = user_info.get("username", "Unknown")
await activity_logger.update_user_activity(username)
except:
pass
users = await activity_logger.get_active_users(timeout_minutes=30)
return {"users": users}
@router.get("/activity_logs")
async def get_activity_logs(request: Request, limit: int = 100):
"""Get recent activity logs"""
# Update current user's activity
try:
user_info = get_current_user(request)
username = user_info.get("username", "Unknown")
await activity_logger.update_user_activity(username)
except:
pass
logs = await activity_logger.get_logs(limit=limit)
return {"logs": logs}
@router.post("/log_action")
async def log_action(request: Request, req: LogActionRequest):
"""Log a user action"""
user_info = get_current_user(request)
username = user_info.get("username", "Unknown")
await activity_logger.log_action(username, req.action, req.details)
return {"status": "ok"}

View File

@@ -20,7 +20,7 @@ class LoginRequest(BaseModel):
def get_client_ip(request: Request) -> str:
"""Get client IP address from request"""
#Get client IP address from request
# Check X-Forwarded-For header first (for proxy/load balancer)
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
@@ -29,7 +29,7 @@ def get_client_ip(request: Request) -> str:
def is_locked_out(ip: str) -> bool:
"""Check if IP is currently locked out"""
#Check if IP is currently locked out
if ip not in failed_attempts:
return False
@@ -49,7 +49,7 @@ def is_locked_out(ip: str) -> bool:
def record_failed_attempt(ip: str):
"""Record a failed login attempt"""
#Record a failed login attempt
now = datetime.now()
if ip not in failed_attempts:
@@ -64,13 +64,13 @@ def record_failed_attempt(ip: str):
def clear_failed_attempts(ip: str):
"""Clear failed attempts for an IP after successful login"""
#Clear failed attempts for an IP after successful login
if ip in failed_attempts:
del failed_attempts[ip]
def create_jwt_token(username: str) -> str:
"""Create a JWT token for the user"""
#Create a JWT token for the user
expiration = datetime.utcnow() + timedelta(days=7) # Token valid for 7 days
payload = {
"username": username,
@@ -81,7 +81,7 @@ def create_jwt_token(username: str) -> str:
def verify_jwt_token(token: str) -> dict:
"""Verify and decode a JWT token"""
#Verify and decode a JWT token
try:
payload = jwt.decode(token, config_module.JWT_SECRET, algorithms=["HS256"])
return payload
@@ -93,7 +93,7 @@ def verify_jwt_token(token: str) -> dict:
@router.post("/login")
async def login(request: Request, response: Response, req: LoginRequest):
"""Login endpoint with rate limiting"""
#Login endpoint with rate limiting
client_ip = get_client_ip(request)
# Check if IP is locked out
@@ -144,14 +144,14 @@ async def login(request: Request, response: Response, req: LoginRequest):
@router.post("/logout")
async def logout(response: Response):
"""Logout endpoint"""
#Logout endpoint
response.delete_cookie("auth_token")
return {"status": "success"}
@router.get("/status")
async def auth_status(request: Request):
"""Check authentication status"""
#Check authentication status
token = request.cookies.get("auth_token")
if not token:

View File

@@ -20,7 +20,7 @@ def set_assignment_manager(manager: BotAssignmentManager):
@router.get("/bot_status")
async def api_bot_status():
"""Get current bot status"""
#Get current bot status
active_count = len(assignment_manager.active_targets) if assignment_manager else 0
return {
"bot_running": STATE.bot_running,

View File

@@ -10,7 +10,7 @@ router = APIRouter(prefix="/api", tags=["config"])
def reload_config_from_file():
"""Reload config values from JSON into module globals"""
#Reload config values from JSON into module globals
path = Path("data/config.json")
if not path.exists():
return
@@ -29,7 +29,7 @@ def reload_config_from_file():
@router.get("/config")
async def get_config():
"""Get all config values (with sensitive values masked)"""
#Get all config values (with sensitive values masked)
path = Path("data/config.json")
# Default config values from config.py
@@ -70,7 +70,7 @@ async def get_config():
@router.post("/config")
async def update_config(req: ConfigUpdateRequest):
"""Update a single config value"""
#Update a single config value
path = Path("data/config.json")
# Valid config keys (from config.py)

View File

@@ -14,14 +14,14 @@ assignment_manager: Optional[BotAssignmentManager] = None
def set_assignment_manager(manager: BotAssignmentManager):
"""Set the global assignment manager reference."""
#Set the global assignment manager reference.
global assignment_manager
assignment_manager = manager
@router.get("/discord_mappings")
async def get_discord_mappings():
"""Get all Torn ID to Discord ID mappings"""
#Get all Torn ID to Discord ID mappings
path = Path("data/discord_mapping.json")
if not path.exists():
return {"mappings": {}}
@@ -33,7 +33,7 @@ async def get_discord_mappings():
@router.post("/discord_mapping")
async def add_discord_mapping(req: DiscordMappingRequest):
"""Add or update a Torn ID to Discord ID mapping"""
#Add or update a Torn ID to Discord ID mapping
path = Path("data/discord_mapping.json")
# Load existing mappings
@@ -59,7 +59,7 @@ async def add_discord_mapping(req: DiscordMappingRequest):
@router.delete("/discord_mapping/{torn_id}")
async def remove_discord_mapping(torn_id: int):
"""Remove a Discord mapping"""
#Remove a Discord mapping
path = Path("data/discord_mapping.json")
if not path.exists():

View File

@@ -1,11 +1,18 @@
"""Faction data population and status management endpoints."""
#Faction data population and status management endpoints.
import json
from pathlib import Path
from fastapi import APIRouter
from models import FactionRequest
from services.server_state import STATE
from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
from services.torn_api import (
populate_friendly,
populate_enemy,
start_friendly_status_loop,
start_enemy_status_loop,
stop_friendly_status_loop,
stop_enemy_status_loop
)
from utils import load_json_list
router = APIRouter(prefix="/api", tags=["factions"])
@@ -73,3 +80,30 @@ async def api_enemy_status():
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
@router.post("/stop_friendly_status")
async def api_stop_friendly_status():
await stop_friendly_status_loop()
return {"status": "friendly status loop stopped"}
@router.post("/stop_enemy_status")
async def api_stop_enemy_status():
await stop_enemy_status_loop()
return {"status": "enemy status loop stopped"}
@router.get("/dashboard_state")
async def get_dashboard_state():
"""Get current dashboard state for restoring UI on page load"""
return {
"friendly_faction_id": STATE.friendly_faction_id,
"enemy_faction_id": STATE.enemy_faction_id,
"friendly_members": [m.model_dump() for m in STATE.friendly.values()],
"enemy_members": [m.model_dump() for m in STATE.enemy.values()],
"friendly_status_interval": STATE.friendly_status_interval,
"enemy_status_interval": STATE.enemy_status_interval,
"friendly_status_running": STATE.friendly_status_running,
"enemy_status_running": STATE.enemy_status_running
}

View File

@@ -10,7 +10,7 @@ templates = Jinja2Templates(directory="templates")
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""Login page"""
#Login page
# If already authenticated, redirect to dashboard
if check_auth(request):
return RedirectResponse(url="/", status_code=302)
@@ -19,7 +19,7 @@ async def login_page(request: Request):
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
"""Dashboard page - requires authentication"""
#Dashboard page - requires authentication
if not check_auth(request):
return RedirectResponse(url="/login", status_code=302)
print(">>> DASHBOARD ROUTE LOADED")
@@ -28,7 +28,15 @@ async def dashboard(request: Request):
@router.get("/config", response_class=HTMLResponse)
async def config_page(request: Request):
"""Config page - requires authentication"""
#Config page - requires authentication
if not check_auth(request):
return RedirectResponse(url="/login", status_code=302)
return templates.TemplateResponse("config.html", {"request": request})
@router.get("/users-log", response_class=HTMLResponse)
async def users_log_page(request: Request):
#Users/Log page - requires authentication
if not check_auth(request):
return RedirectResponse(url="/login", status_code=302)
return templates.TemplateResponse("users_log.html", {"request": request})