File size: 6,112 Bytes
d60934b
 
 
 
 
 
 
 
 
 
5873878
d60934b
 
 
 
 
 
 
 
 
 
a5399cd
 
 
d60934b
 
 
5873878
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d60934b
 
 
 
 
 
 
 
 
 
 
 
 
a5399cd
 
d60934b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5399cd
d60934b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5873878
d60934b
5873878
d60934b
5873878
 
 
 
 
 
 
 
 
d60934b
5873878
 
 
d60934b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
from fastapi import FastAPI, Request, HTTPException, Security, Depends
from fastapi.middleware.cors import CORSMiddleware
from app.routes import health, wagons, chat, players, generate
from app.core.logging import get_logger, setup_logging
from dotenv import load_dotenv
from datetime import datetime
import time
from pathlib import Path
from fastapi.security.api_key import APIKeyHeader
import os
import secrets

# Load environment variables
load_dotenv()

# Setup logging
logger = get_logger("main")

API_KEY = os.getenv("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True)

# Add this near the top with other environment variables
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")  # Add default for local development


async def get_api_key(api_key_header: str = Security(api_key_header)):
    if not API_KEY:
        logger.error("API key not configured on server")
        raise HTTPException(
            status_code=500,
            detail="Server configuration error"  # Don't expose specific details
        )

    if not api_key_header:
        raise HTTPException(
            status_code=401,
            detail="Missing API key",
            headers={"WWW-Authenticate": "ApiKey"},
        )

    if not secrets.compare_digest(api_key_header, API_KEY):  # Constant-time comparison
        logger.warning(f"Invalid API key attempt from {request.client.host}")
        raise HTTPException(
            status_code=403,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "ApiKey"},
        )

    return api_key_header


app = FastAPI(
    title="Game Jam API",
    description="API for Game Jam Hackathon",
    version="1.0.0",
    dependencies=[Depends(get_api_key)],
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[FRONTEND_URL],  # Replace "*" with specific frontend URL
    allow_credentials=True,  # Changed to True since we're restricting origins
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
    allow_headers=["*"],
    expose_headers=[
        "Location",
        "Access-Control-Allow-Origin",
        "Access-Control-Allow-Methods",
        "Access-Control-Allow-Headers",
        "Access-Control-Allow-Credentials",
        "Access-Control-Expose-Headers",
    ],
    max_age=3600,
)


@app.middleware("http")
async def handle_redirects(request: Request, call_next):
    """Ensure CORS headers are in redirect responses and force https in the 'Location' header."""
    response = await call_next(request)

    response.headers["Access-Control-Allow-Origin"] = FRONTEND_URL 
    response.headers["Access-Control-Allow-Methods"] = (
        "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH"
    )
    response.headers["Access-Control-Allow-Headers"] = "*"
    response.headers["Access-Control-Max-Age"] = "3600"

    if response.status_code in [301, 302, 307, 308]:
        response.headers["Access-Control-Expose-Headers"] = "Location"
        if "Location" in response.headers:
            location = response.headers["Location"]
            if location.startswith("http://"):
                response.headers["Location"] = location.replace(
                    "http://", "https://", 1
                )

    return response


@app.middleware("http")
async def security_headers(request: Request, call_next):
    """Add security-related headers to all responses."""
    response = await call_next(request)

    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"  # Stricter than SAMEORIGIN
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
        "style-src 'self' 'unsafe-inline'; "
        "img-src 'self' data: https:; "
        "connect-src 'self' "
    )
    response.headers["Permissions-Policy"] = (
        "accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
        "magnetometer=(), microphone=(), payment=(), usb=()"
    )
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"

    return response


@app.middleware("http")
async def log_requests(request: Request, call_next):
    """Middleware to log all requests and responses."""
    start_time = time.time()

    logger.info(
        "Incoming request",
        extra={
            "method": request.method,
            "url": str(request.url),
            "client_host": request.client.host if request.client else None,
            "timestamp": datetime.utcnow().isoformat(),
        },
    )

    try:
        response = await call_next(request)
        process_time = time.time() - start_time

        logger.info(
            "Request completed",
            extra={
                "method": request.method,
                "url": str(request.url),
                "status_code": response.status_code,
                "process_time_ms": round(process_time * 1000, 2),
            },
        )
        return response

    except Exception as e:
        logger.error(
            "Request failed",
            extra={"method": request.method, "url": str(request.url), "error": str(e)},
        )
        raise


app.include_router(health.router)
app.include_router(wagons.router)
app.include_router(chat.router)
app.include_router(players.router)
app.include_router(generate.router)


@app.get("/")
async def root():
    logger.info("Root endpoint accessed")
    return {
        "message": "Welcome to Game Jam API",
        "docs_url": "/docs",
        "health_check": "/health",
        "wagons_endpoint": "/api/wagons",
        "chat_endpoint": "/api/chat",
        "players_endpoint": "/api/players",
    }


@app.on_event("startup")
async def startup_event():
    logs_dir = Path("logs")
    logs_dir.mkdir(exist_ok=True)

    setup_logging()