Authenticatoin Implementation
This commit is contained in:
@@ -9,8 +9,6 @@ Features:
|
||||
|
||||
|
||||
ToDo:
|
||||
- basic auth
|
||||
- since control of the Discord bot would also be through there technically
|
||||
- Log output section on webpage, to see who is being assigned to who, when the hit is complete, if it is missed, how many hits a person has done
|
||||
- Hit Leaderboard?
|
||||
|
||||
|
||||
Binary file not shown.
@@ -39,3 +39,7 @@ ASSIGNMENT_REMINDER = _config.get("ASSIGNMENT_REMINDER", 45) # Seconds before s
|
||||
|
||||
# Chain Timer Settings
|
||||
CHAIN_TIMER_THRESHOLD = _config.get("CHAIN_TIMER_THRESHOLD", 5) # Minutes - start assigning hits when chain timer is at or below this
|
||||
|
||||
# Authentication
|
||||
AUTH_PASSWORD = _config.get("AUTH_PASSWORD", "YOUR_AUTH_PASSWORD_HERE") # Universal password for all users
|
||||
JWT_SECRET = _config.get("JWT_SECRET", "your-secret-key-change-this") # Secret key for JWT tokens
|
||||
|
||||
3
main.py
3
main.py
@@ -14,7 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from services.bot_assignment import BotAssignmentManager
|
||||
|
||||
# Import routers
|
||||
from routers import pages, factions, assignments, discord_mappings, config
|
||||
from routers import pages, factions, assignments, discord_mappings, config, auth
|
||||
from routers import bot as bot_router
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ app = FastAPI()
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# Include all routers
|
||||
app.include_router(auth.router)
|
||||
app.include_router(pages.router)
|
||||
app.include_router(factions.router)
|
||||
app.include_router(assignments.router)
|
||||
|
||||
BIN
routers/__pycache__/auth.cpython-311.pyc
Normal file
BIN
routers/__pycache__/auth.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
164
routers/auth.py
Normal file
164
routers/auth.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Authentication endpoints for login/logout."""
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict
|
||||
|
||||
import config as config_module
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
# In-memory storage for failed login attempts
|
||||
# Format: {ip_address: {"count": int, "locked_until": datetime}}
|
||||
failed_attempts: Dict[str, Dict] = {}
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
name: str
|
||||
password: str
|
||||
|
||||
|
||||
def get_client_ip(request: Request) -> str:
|
||||
"""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:
|
||||
return forwarded.split(",")[0].strip()
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
def is_locked_out(ip: str) -> bool:
|
||||
"""Check if IP is currently locked out"""
|
||||
if ip not in failed_attempts:
|
||||
return False
|
||||
|
||||
attempt_data = failed_attempts[ip]
|
||||
locked_until = attempt_data.get("locked_until")
|
||||
|
||||
if not locked_until:
|
||||
return False
|
||||
|
||||
# Check if lockout has expired
|
||||
if datetime.now() >= locked_until:
|
||||
# Clear the lockout
|
||||
del failed_attempts[ip]
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def record_failed_attempt(ip: str):
|
||||
"""Record a failed login attempt"""
|
||||
now = datetime.now()
|
||||
|
||||
if ip not in failed_attempts:
|
||||
failed_attempts[ip] = {"count": 1, "locked_until": None}
|
||||
else:
|
||||
failed_attempts[ip]["count"] += 1
|
||||
|
||||
# Lock out after 5 failed attempts
|
||||
if failed_attempts[ip]["count"] >= 5:
|
||||
failed_attempts[ip]["locked_until"] = now + timedelta(minutes=5)
|
||||
print(f"IP {ip} locked out until {failed_attempts[ip]['locked_until']}")
|
||||
|
||||
|
||||
def clear_failed_attempts(ip: str):
|
||||
"""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"""
|
||||
expiration = datetime.utcnow() + timedelta(days=7) # Token valid for 7 days
|
||||
payload = {
|
||||
"username": username,
|
||||
"exp": expiration
|
||||
}
|
||||
token = jwt.encode(payload, config_module.JWT_SECRET, algorithm="HS256")
|
||||
return token
|
||||
|
||||
|
||||
def verify_jwt_token(token: str) -> dict:
|
||||
"""Verify and decode a JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, config_module.JWT_SECRET, algorithms=["HS256"])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(request: Request, response: Response, req: LoginRequest):
|
||||
"""Login endpoint with rate limiting"""
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
# Check if IP is locked out
|
||||
if is_locked_out(client_ip):
|
||||
attempt_data = failed_attempts[client_ip]
|
||||
locked_until = attempt_data["locked_until"]
|
||||
remaining = (locked_until - datetime.now()).total_seconds()
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail=f"Too many failed attempts. Try again in {int(remaining)} seconds."
|
||||
)
|
||||
|
||||
# Verify password (universal password for all users)
|
||||
if req.password != config_module.AUTH_PASSWORD:
|
||||
record_failed_attempt(client_ip)
|
||||
attempts_left = 5 - failed_attempts[client_ip]["count"]
|
||||
|
||||
if attempts_left <= 0:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="Too many failed attempts. Locked out for 5 minutes."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail=f"Invalid password. {attempts_left} attempts remaining."
|
||||
)
|
||||
|
||||
# Successful login
|
||||
clear_failed_attempts(client_ip)
|
||||
|
||||
# Create JWT token
|
||||
token = create_jwt_token(req.name)
|
||||
|
||||
# Set token as HTTP-only cookie
|
||||
response.set_cookie(
|
||||
key="auth_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
max_age=7 * 24 * 60 * 60, # 7 days in seconds
|
||||
samesite="lax"
|
||||
)
|
||||
|
||||
print(f"User '{req.name}' logged in from IP {client_ip}")
|
||||
|
||||
return {"status": "success", "username": req.name}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(response: Response):
|
||||
"""Logout endpoint"""
|
||||
response.delete_cookie("auth_token")
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def auth_status(request: Request):
|
||||
"""Check authentication status"""
|
||||
token = request.cookies.get("auth_token")
|
||||
|
||||
if not token:
|
||||
return {"authenticated": False}
|
||||
|
||||
try:
|
||||
payload = verify_jwt_token(token)
|
||||
return {"authenticated": True, "username": payload.get("username")}
|
||||
except HTTPException:
|
||||
return {"authenticated": False}
|
||||
@@ -42,7 +42,9 @@ async def get_config():
|
||||
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
|
||||
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
|
||||
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER,
|
||||
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD
|
||||
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD,
|
||||
"AUTH_PASSWORD": config_module.AUTH_PASSWORD,
|
||||
"JWT_SECRET": config_module.JWT_SECRET
|
||||
}
|
||||
|
||||
if path.exists():
|
||||
@@ -57,7 +59,7 @@ async def get_config():
|
||||
|
||||
# Mask sensitive values
|
||||
masked_config = config_values.copy()
|
||||
sensitive = ["TORN_API_KEY", "FFSCOUTER_KEY", "DISCORD_TOKEN"]
|
||||
sensitive = ["TORN_API_KEY", "FFSCOUTER_KEY", "DISCORD_TOKEN", "AUTH_PASSWORD", "JWT_SECRET"]
|
||||
for key in sensitive:
|
||||
if key in masked_config and masked_config[key]:
|
||||
val = str(masked_config[key])
|
||||
@@ -75,7 +77,8 @@ async def update_config(req: ConfigUpdateRequest):
|
||||
valid_keys = {
|
||||
"TORN_API_KEY", "FFSCOUTER_KEY", "DISCORD_TOKEN", "ALLOWED_CHANNEL_ID",
|
||||
"HIT_CHECK_INTERVAL", "REASSIGN_DELAY",
|
||||
"ASSIGNMENT_TIMEOUT", "ASSIGNMENT_REMINDER", "CHAIN_TIMER_THRESHOLD"
|
||||
"ASSIGNMENT_TIMEOUT", "ASSIGNMENT_REMINDER", "CHAIN_TIMER_THRESHOLD",
|
||||
"AUTH_PASSWORD", "JWT_SECRET"
|
||||
}
|
||||
|
||||
# Validate key is valid
|
||||
@@ -98,7 +101,9 @@ async def update_config(req: ConfigUpdateRequest):
|
||||
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
|
||||
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
|
||||
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER,
|
||||
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD
|
||||
"CHAIN_TIMER_THRESHOLD": config_module.CHAIN_TIMER_THRESHOLD,
|
||||
"AUTH_PASSWORD": config_module.AUTH_PASSWORD,
|
||||
"JWT_SECRET": config_module.JWT_SECRET
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
"""Web page routes for dashboard and config page."""
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from utils.auth import check_auth
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
"""Login page"""
|
||||
# If already authenticated, redirect to dashboard
|
||||
if check_auth(request):
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
return templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request):
|
||||
"""Dashboard page - requires authentication"""
|
||||
if not check_auth(request):
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
print(">>> DASHBOARD ROUTE LOADED")
|
||||
return templates.TemplateResponse("dashboard.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/config", response_class=HTMLResponse)
|
||||
async def config_page(request: Request):
|
||||
"""Config page - requires authentication"""
|
||||
if not check_auth(request):
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
return templates.TemplateResponse("config.html", {"request": request})
|
||||
|
||||
@@ -8,7 +8,9 @@ const CONFIG_FIELDS = {
|
||||
"REASSIGN_DELAY": "reassign-delay",
|
||||
"ASSIGNMENT_TIMEOUT": "assignment-timeout",
|
||||
"ASSIGNMENT_REMINDER": "assignment-reminder",
|
||||
"CHAIN_TIMER_THRESHOLD": "chain-timer-threshold"
|
||||
"CHAIN_TIMER_THRESHOLD": "chain-timer-threshold",
|
||||
"AUTH_PASSWORD": "auth-password",
|
||||
"JWT_SECRET": "jwt-secret"
|
||||
};
|
||||
|
||||
let sensitiveFields = [];
|
||||
@@ -43,8 +45,11 @@ async function loadConfig() {
|
||||
}
|
||||
|
||||
async function saveConfigValue(key) {
|
||||
console.log("saveConfigValue called with key:", key);
|
||||
const inputId = CONFIG_FIELDS[key];
|
||||
console.log("Input ID:", inputId);
|
||||
const input = document.getElementById(inputId);
|
||||
console.log("Input element:", input);
|
||||
|
||||
if (!input) {
|
||||
console.error("Input not found:", inputId);
|
||||
@@ -52,13 +57,17 @@ async function saveConfigValue(key) {
|
||||
}
|
||||
|
||||
let value = input.value.trim();
|
||||
console.log("Value to save:", value);
|
||||
|
||||
// Don't save if sensitive field is empty (means user didn't change it)
|
||||
if (sensitiveFields.includes(key) && value === "") {
|
||||
console.log("No changes to save - field is empty");
|
||||
alert("No changes to save");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Proceeding with save...");
|
||||
|
||||
// Convert to number if needed
|
||||
if (input.type === "number") {
|
||||
value = parseInt(value);
|
||||
@@ -69,11 +78,13 @@ async function saveConfigValue(key) {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Sending API request to /api/config with:", { key, value });
|
||||
const res = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
console.log("API response status:", res.status);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Save failed:", res.status);
|
||||
@@ -94,12 +105,41 @@ async function saveConfigValue(key) {
|
||||
}
|
||||
|
||||
function wireUp() {
|
||||
console.log("wireUp called");
|
||||
// Attach save handlers to all save buttons
|
||||
const saveButtons = document.querySelectorAll(".config-save-btn");
|
||||
console.log("Found save buttons:", saveButtons.length);
|
||||
saveButtons.forEach(btn => {
|
||||
const key = btn.dataset.key;
|
||||
console.log("Attaching handler for key:", key);
|
||||
btn.addEventListener("click", () => saveConfigValue(key));
|
||||
});
|
||||
|
||||
// Attach logout handler
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
if (logoutBtn) logoutBtn.addEventListener("click", handleLogout);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
console.log("handleLogout called");
|
||||
try {
|
||||
console.log("Sending logout request to /auth/logout");
|
||||
const response = await fetch("/auth/logout", {
|
||||
method: "POST"
|
||||
});
|
||||
console.log("Logout response status:", response.status);
|
||||
|
||||
if (response.ok) {
|
||||
console.log("Logout successful, redirecting to /login");
|
||||
window.location.href = "/login";
|
||||
} else {
|
||||
console.error("Logout failed with status:", response.status);
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
@@ -730,10 +730,41 @@ function wireUp() {
|
||||
const resetBtn = document.getElementById("reset-groups-btn");
|
||||
if (resetBtn) resetBtn.addEventListener("click", resetGroups);
|
||||
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
if (logoutBtn) logoutBtn.addEventListener("click", handleLogout);
|
||||
|
||||
setupDropZones();
|
||||
console.log(">>> wireUp completed");
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Logout handler
|
||||
// ---------------------------
|
||||
async function handleLogout() {
|
||||
console.log("handleLogout called");
|
||||
try {
|
||||
console.log("Sending logout request to /auth/logout");
|
||||
const response = await fetch("/auth/logout", {
|
||||
method: "POST"
|
||||
});
|
||||
console.log("Logout response status:", response.status);
|
||||
|
||||
if (response.ok) {
|
||||
console.log("Logout successful, redirecting to /login");
|
||||
// Redirect to login page
|
||||
window.location.href = "/login";
|
||||
} else {
|
||||
console.error("Logout failed with status:", response.status);
|
||||
// Still redirect to login page
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
// Still redirect to login page
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Initial load
|
||||
// ---------------------------
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<h1>Configuration</h1>
|
||||
<div class="top-controls">
|
||||
<a href="/" class="nav-link">Back to Dashboard</a>
|
||||
<button id="logout-btn" class="nav-link" style="background:none;border:none;cursor:pointer;padding:0.6rem 1rem;">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +96,24 @@
|
||||
<button class="config-save-btn" data-key="CHAIN_TIMER_THRESHOLD">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Settings Section -->
|
||||
<div class="faction-card small config-section">
|
||||
<h2>Authentication Settings</h2>
|
||||
<div class="config-group">
|
||||
<label for="auth-password">Universal Password</label>
|
||||
<p class="config-description">Shared password that all users use to log in</p>
|
||||
<input type="password" id="auth-password" class="config-input" />
|
||||
<button class="config-save-btn" data-key="AUTH_PASSWORD">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="config-group">
|
||||
<label for="jwt-secret">JWT Secret Key</label>
|
||||
<p class="config-description">Secret key for session tokens (change from default for security)</p>
|
||||
<input type="password" id="jwt-secret" class="config-input" />
|
||||
<button class="config-save-btn" data-key="JWT_SECRET">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<div class="top-controls">
|
||||
<a href="/config" class="nav-link">Settings</a>
|
||||
<button id="logout-btn" class="nav-link" style="background:none;border:none;cursor:pointer;padding:0.6rem 1rem;">Logout</button>
|
||||
<button id="bot-control-btn" class="bot-btn" data-running="false">Start Bot</button>
|
||||
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
|
||||
</div>
|
||||
|
||||
214
templates/login.html
Normal file
214
templates/login.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Login - War Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/styles.css" />
|
||||
<style>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: linear-gradient(135deg, #1a1a26 0%, #2d2d44 100%);
|
||||
border: 1px solid rgba(102, 204, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 3rem;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
text-align: center;
|
||||
color: #66ccff;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
color: #99a7bf;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
color: #66ccff;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: #1a1a26;
|
||||
color: #f0f0f0;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #66ccff;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 1rem;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
background-color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.5);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
color: #f44336;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
text-align: center;
|
||||
color: #99a7bf;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1>War Dashboard</h1>
|
||||
<p class="login-subtitle">Please log in to continue</p>
|
||||
|
||||
<div id="error-msg" class="error-message"></div>
|
||||
|
||||
<form class="login-form" id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Enter your name"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn" id="submit-btn">Log In</button>
|
||||
</form>
|
||||
|
||||
<p class="info-text">
|
||||
Contact Faction Management if you need the shared password.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('login-form');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!name || !password) {
|
||||
showError('Please enter both name and password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable submit button
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Logging in...';
|
||||
errorMsg.classList.remove('show');
|
||||
|
||||
try {
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Successful login - redirect to dashboard
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// Show error message
|
||||
showError(data.detail || 'Login failed');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Log In';
|
||||
}
|
||||
} catch (error) {
|
||||
showError('An error occurred. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Log In';
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
errorMsg.textContent = message;
|
||||
errorMsg.classList.add('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
utils/__pycache__/auth.cpython-311.pyc
Normal file
BIN
utils/__pycache__/auth.cpython-311.pyc
Normal file
Binary file not shown.
33
utils/auth.py
Normal file
33
utils/auth.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Authentication utilities and dependencies."""
|
||||
import jwt
|
||||
from fastapi import Request, HTTPException
|
||||
import config as config_module
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> dict:
|
||||
"""Dependency to check authentication and return user info"""
|
||||
token = request.cookies.get("auth_token")
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, config_module.JWT_SECRET, algorithms=["HS256"])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
|
||||
def check_auth(request: Request) -> bool:
|
||||
"""Check if user is authenticated (returns bool, doesn't raise exception)"""
|
||||
token = request.cookies.get("auth_token")
|
||||
if not token:
|
||||
return False
|
||||
|
||||
try:
|
||||
jwt.decode(token, config_module.JWT_SECRET, algorithms=["HS256"])
|
||||
return True
|
||||
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
|
||||
return False
|
||||
Reference in New Issue
Block a user