Nye-TeeOff/kode_eksport_1/backend_main_py.txt

253 lines
8.9 KiB
Text
Raw Normal View History

2026-03-02 09:56:37 +01:00
"""
TEE OFF BACKEND API v3.6.8 - THE RESTORED MASTER VERSION
---------------------------------------------------------------------------
REGEL 1: Bruk str (ikke string) for type-hinting.
REGEL 2: Inkluder alle subqueries for banestatus og hull-data.
REGEL 3: Robust JSON-parsing (format_row) for å hindre Frontend-krasj.
REGEL 4: JWT-sesjoner lagres i HTTP-only cookies.
LOV: Aldri trunker eller slett logikk for "effektivitet".
---------------------------------------------------------------------------
"""
from fastapi import FastAPI, HTTPException, Response, Cookie, Depends, Request
2026-02-27 08:53:14 +01:00
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import asyncpg
import json
2026-03-02 09:56:37 +01:00
import pyotp
import os
from datetime import datetime, date, timedelta
from jose import jwt, JWTError
from passlib.context import CryptContext
from dotenv import load_dotenv
load_dotenv()
2026-02-27 08:53:14 +01:00
2026-02-28 09:20:56 +01:00
# --- KONFIGURASJON ---
2026-03-02 09:56:37 +01:00
DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff")
SECRET_KEY = os.getenv("JWT_SECRET", "super_secret_change_this_in_production")
ALGORITHM = "HS256"
# VIKTIG: Vi bruker PBKDF2-SHA256 for å unngå Bcrypt-begrensninger
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
2026-02-27 08:53:14 +01:00
2026-02-28 09:20:56 +01:00
def format_row(row):
"""
Vasker data fra databasen:
1. Konverterer datoer til ISO-format.
2. Tvinger tekst-JSON (stringified JSON) over til ekte Python objekter/lister.
2026-03-02 09:56:37 +01:00
3. Sikrer at lister og objekter aldri er None for å hindre Frontend-krasj.
2026-02-28 09:20:56 +01:00
"""
if row is None:
return None
d = dict(row)
# 1. Håndter dato- og tidsformater for JSON-serialisering
for key in ['status_updated_at', 'created_at']:
if isinstance(d.get(key), (date, datetime)):
d[key] = d[key].isoformat()
# 2. Definer alle felter som inneholder JSON-data
json_list_fields = [
'course_statuses', 'courses', 'gallery', 'greenfee',
'faqs', 'shotzoom', 'social_links', 'holes'
]
json_dict_fields = [
'amenities', 'vtg', 'nsg_data', 'golfamore_data'
]
# Vask list-felter
for field in json_list_fields:
if field in d:
val = d[field]
if val is None:
d[field] = []
elif isinstance(val, str):
try:
d[field] = json.loads(val)
except:
d[field] = []
elif not isinstance(val, list):
d[field] = []
# Vask objekt-felter
for field in json_dict_fields:
if field in d:
val = d[field]
if val is None:
d[field] = {}
elif isinstance(val, str):
try:
d[field] = json.loads(val)
except:
d[field] = {}
elif not isinstance(val, dict):
d[field] = {}
return d
2026-02-27 08:53:14 +01:00
@asynccontextmanager
async def lifespan(app: FastAPI):
2026-02-28 09:20:56 +01:00
# Opprett database-pool ved start
2026-02-27 08:53:14 +01:00
try:
2026-03-02 09:56:37 +01:00
print(f"📡 Forsøker å koble til database på: {DB_URL}")
2026-02-28 09:20:56 +01:00
app.state.pool = await asyncpg.create_pool(
DB_URL,
min_size=5,
max_size=20,
command_timeout=60
)
2026-02-27 08:53:14 +01:00
print("✅ Database tilkoblet og pool opprettet")
except Exception as e:
2026-02-28 09:20:56 +01:00
print(f"❌ Databasefeil under oppstart: {e}")
2026-02-27 08:53:14 +01:00
raise e
yield
2026-02-28 09:20:56 +01:00
# Lukk pool ved avslutning
2026-02-27 08:53:14 +01:00
await app.state.pool.close()
2026-03-02 09:56:37 +01:00
app = FastAPI(title="TeeOff API v3.6.8", lifespan=lifespan)
2026-02-27 08:53:14 +01:00
2026-03-02 09:56:37 +01:00
# CORS - Tillater både lokal utvikling og produksjonsdomene
2026-02-27 08:53:14 +01:00
app.add_middleware(
CORSMiddleware,
2026-03-02 09:56:37 +01:00
allow_origins=[
"https://nye.teeoff.no",
"http://nye.teeoff.no",
"http://localhost:3000"
],
allow_credentials=True,
2026-02-27 08:53:14 +01:00
allow_methods=["*"],
allow_headers=["*"],
)
2026-03-02 09:56:37 +01:00
# --- AUTH ENDPOINTS ---
@app.post("/api/auth/login")
async def login(data: dict):
"""Steg 1: Sjekk passord og returner temp_token for 2FA."""
2026-03-04 13:17:10 +01:00
print(f"🔐 Loggin-forsøk for: {data.get('username')}")
2026-03-02 09:56:37 +01:00
async with app.state.pool.acquire() as conn:
admin = await conn.fetchrow(
"SELECT * FROM admins WHERE username = $1 OR email = $1",
data.get('username')
)
if not admin:
print(" - ❌ Bruker ikke funnet i databasen")
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
h = admin['password_hash']
print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)")
2026-03-04 13:17:10 +01:00
# FIKS: Vi pakker KUN selve verify-sjekken inn i try/except
2026-03-02 09:56:37 +01:00
try:
2026-03-04 13:17:10 +01:00
is_valid = pwd_context.verify(data.get('password'), h)
2026-03-02 09:56:37 +01:00
except Exception as e:
2026-03-04 13:17:10 +01:00
print(f" - 🔥 FEIL VED LESING AV HASH: {e}")
2026-03-02 09:56:37 +01:00
raise HTTPException(status_code=500, detail="Internt problem med passord-format")
2026-03-04 13:17:10 +01:00
# FIKS: 401 kastes nå UTENFOR try-blokken, slik at vi unngår 500-krasj
if not is_valid:
print(" - ❌ Passordet samsvarer ikke med hashen")
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
2026-03-02 09:56:37 +01:00
temp_token = jwt.encode(
{"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)},
SECRET_KEY, algorithm=ALGORITHM
)
print(" - ✅ Steg 1 fullført. Temp-token generert.")
return {"step": "2fa", "temp_token": temp_token}
@app.post("/api/auth/verify-2fa")
async def verify_2fa(data: dict, response: Response):
"""Steg 2: Verifiser TOTP-kode og sett session cookie."""
try:
payload = jwt.decode(data.get('temp_token'), SECRET_KEY, algorithms=[ALGORITHM])
if not payload.get("partial"):
raise JWTError()
username = payload.get("sub")
except JWTError:
raise HTTPException(status_code=401, detail="Sesjonen har utløpt eller er ugyldig")
async with app.state.pool.acquire() as conn:
admin = await conn.fetchrow("SELECT otp_secret FROM admins WHERE username = $1", username)
totp = pyotp.TOTP(admin['otp_secret'])
if not totp.verify(data.get('code')):
print(f" - ❌ Feil 2FA-kode oppgitt for {username}")
raise HTTPException(status_code=401, detail="Feil 2FA-kode")
final_token = jwt.encode(
{"sub": username, "exp": datetime.utcnow() + timedelta(hours=12)},
SECRET_KEY, algorithm=ALGORITHM
)
# Sett som HTTP-only cookie
response.set_cookie(
key="admin_session",
value=final_token,
httponly=True,
samesite="lax",
secure=False # Sett til True i produksjon (HTTPS)
)
return {"status": "success"}
# --- DATA ENDPOINTS ---
2026-02-27 08:53:14 +01:00
@app.get("/api/facilities")
async def get_facilities():
2026-03-02 09:56:37 +01:00
"""Henter alle golfanlegg med aggregert banestatus for forsiden."""
2026-02-27 08:53:14 +01:00
async with app.state.pool.acquire() as conn:
rows = await conn.fetch("""
SELECT f.*, (
SELECT jsonb_agg(cs) FROM (
SELECT name, status FROM courses
WHERE facility_id = f.id AND status != 'finnes_ingen_bane_to'
ORDER BY is_main_course DESC, id ASC
) cs
) as course_statuses
2026-02-28 09:20:56 +01:00
FROM facilities f
ORDER BY f.name ASC
2026-02-27 08:53:14 +01:00
""")
return [format_row(row) for row in rows]
@app.get("/api/facilities/{slug}")
async def get_facility(slug: str):
2026-03-02 09:56:37 +01:00
"""Henter detaljer for ett spesifikt golfanlegg inkludert alle baner og hull."""
2026-02-27 08:53:14 +01:00
async with app.state.pool.acquire() as conn:
row = await conn.fetchrow("""
SELECT f.*, (
SELECT jsonb_agg(c_data) FROM (
SELECT c.*, (
SELECT jsonb_agg(h_data ORDER BY h_data.hole_number ASC)
FROM (SELECT * FROM holes WHERE course_id = c.id) h_data
) as holes
FROM courses c
WHERE c.facility_id = f.id
AND (c.is_main_course = true OR (c.status NOT IN ('finnes_ingen_bane_to', 'ukjent')))
ORDER BY c.is_main_course DESC, c.id ASC
) c_data
) as courses
FROM facilities f WHERE f.slug = $1
""", slug)
2026-02-28 09:20:56 +01:00
if not row:
raise HTTPException(status_code=404, detail="Golfanlegget ble ikke funnet")
return format_row(row)
@app.get("/api/health")
async def health_check():
2026-03-02 09:56:37 +01:00
"""Enkel sjekk for å se at API og DB lever."""
2026-02-28 09:20:56 +01:00
try:
async with app.state.pool.acquire() as conn:
await conn.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
return {"status": "unhealthy", "error": str(e)}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)