diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 7204c71..a958cb9 100644 Binary files a/backend/__pycache__/main.cpython-311.pyc and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/create_admin.py b/backend/create_admin.py index f44898f..c2d3dcb 100644 --- a/backend/create_admin.py +++ b/backend/create_admin.py @@ -1,44 +1,64 @@ """ -TEE OFF ADMIN GENERATOR v1.3 (PBKDF2 ONLY) +TEE OFF ADMIN GENERATOR v1.9 (DEBUG & BULLETPROOF) --------------------------------------------------------------------------- -FUNKSJON: Genererer SQL-kommando for PBKDF2-basert administrator. +FUNKSJON: Genererer SQL-kommando for administrator. +STATUS: Beholder TRUNCATE for feilsøking, men sikrer SQL-innsendingen. --------------------------------------------------------------------------- """ import pyotp from passlib.hash import pbkdf2_sha256 import getpass +import sys def generate_admin(): print("\n" + "="*50) - print(" TEE OFF ADMIN GENERATOR v1.3") + print(" TEE OFF ADMIN GENERATOR v1.9 (DEBUG MODE)") print("="*50) username = input("Brukernavn (f.eks Envide Webutvikling): ").strip() email = input("E-post: ").strip() - password = getpass.getpass("Passord: ") + # Sikre mot SQL-feil hvis navnet/eposten inneholder apostrof + safe_username = username.replace("'", "''") + safe_email = email.replace("'", "''") + + # Passord-verifisering + while True: + password = getpass.getpass("Skriv inn passord: ") + password_confirm = getpass.getpass("Gjenta passord: ") + + if password == password_confirm: + if len(password) < 8: + print("⚠️ Advarsel: Passordet bør være minst 8 tegn.") + print(f"\n[DEBUG] Passord akseptert. Lengde: {len(password)} tegn.") + break + else: + print("❌ Passordene er ikke like. Prøv igjen.\n") + otp_secret = pyotp.random_base32() print("⏳ Genererer PBKDF2-hash...") - # Dette vil produsere en streng som starter med $pbkdf2-sha256$... password_hash = pbkdf2_sha256.hash(password) + print(f"[DEBUG] Hash generert. Lengde: {len(password_hash)} tegn.") print("\n✅ GENERERING VELLYKKET!") print("-" * 50) - print("1. KJØR DISSE TO KOMMANDOENE I REKKEFØLGE:") + print("SLIK LEGGER DU INN BRUKEREN TRYGT:") print("-" * 50) - - # Kommando 1: Tøm tabellen for å fjerne gamle, inkompatible hasher - print(f'docker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "TRUNCATE admins;"') - - # Kommando 2: Sett inn den nye brukeren - sql = f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{username}', '{email}', '{password_hash}', '{otp_secret}');" - print(f'\ndocker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "{sql}"') - - print("\n" + "-" * 50) - print("2. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:") - print(f"Nøkkel: {otp_secret}") + print("1. Gå inn i databasen:") + print(" docker exec -it teeoff_db psql -U teeoff_admin -d teeoff") + print("\n2. Lim inn disse to linjene nøyaktig slik de står:") + print("TRUNCATE admins;") + print(f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{safe_username}', '{safe_email}', '{password_hash}', '{otp_secret}');") + print("\n3. Skriv 'exit' for å gå ut.") + print("-" * 50) + print("4. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:") + print(f"Bruk denne nøkkelen: {otp_secret}") print("-" * 50 + "\n") if __name__ == "__main__": - generate_admin() \ No newline at end of file + try: + generate_admin() + except KeyboardInterrupt: + print("\nAvbrutt.") + sys.exit(0) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 58d2a09..d57f6e1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -126,7 +126,7 @@ app.add_middleware( @app.post("/api/auth/login") async def login(data: dict): """Steg 1: Sjekk passord og returner temp_token for 2FA.""" - print(f"🔐 Login-forsøk for: {data.get('username')}") + print(f"🔐 Loggin-forsøk for: {data.get('username')}") async with app.state.pool.acquire() as conn: admin = await conn.fetchrow( @@ -138,17 +138,20 @@ async def login(data: dict): print(" - ❌ Bruker ikke funnet i databasen") raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") - # DEBUG: Printer starten på hashen for å verifisere formatet ($pbkdf2-sha256$...) h = admin['password_hash'] print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)") + # FIKS: Vi pakker KUN selve verify-sjekken inn i try/except try: - if not pwd_context.verify(data.get('password'), h): - print(" - ❌ Passordet samsvarer ikke med hashen") - raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") + is_valid = pwd_context.verify(data.get('password'), h) except Exception as e: - print(f" - 🔥 FEIL VED VERIFISERING: {e}") + print(f" - 🔥 FEIL VED LESING AV HASH: {e}") raise HTTPException(status_code=500, detail="Internt problem med passord-format") + + # 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") temp_token = jwt.encode( {"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)}, diff --git a/backend/scrape_status.py b/backend/scrape_status.py index b3f2397..baf53fd 100644 --- a/backend/scrape_status.py +++ b/backend/scrape_status.py @@ -2,7 +2,7 @@ import asyncio import os import asyncpg import smtplib -import re # Ny import for tekst-vasking +import re from datetime import datetime from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -12,7 +12,6 @@ try: except ImportError: from playwright_stealth import stealth as apply_stealth -from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv load_dotenv() @@ -20,28 +19,21 @@ load_dotenv() DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" def clean_text(text): - """Fjerner spesialtegn og normaliserer tekst for sammenligning""" return re.sub(r'[^a-zA-Z0-9æøåÆØÅ]', '', text).lower() def interpret_status(text, keyword=None): t_raw = text.lower() if keyword: - # Fuzzy match: Vi renser både tekst og søkeord for bindestreker/mellomrom k_clean = clean_text(keyword) - - # Hvis vi ikke finner søkeordet engang i renset form, gi opp if k_clean not in clean_text(t_raw): return "NOT_FOUND" - # Hvis vi finner det, prøv å isolere teksten rundt det originale ordet - # Vi leter etter det originale keywordet først parts = re.split(re.escape(keyword), t_raw, flags=re.IGNORECASE) if len(parts) > 1: t_raw = parts[1][:150] else: - # Fallback hvis keywordet er delt av HTML-tagger (f.eks 18 hull) - t_raw = t_raw[-200:] # Bruk slutten av teksten hvis ordet er vanskelig å isolere + t_raw = t_raw[-200:] if any(word in t_raw for word in ["stengt", "lukket", "frost", "snø", "is", "closed", "stenger"]): return "stengt" @@ -53,28 +45,34 @@ def interpret_status(text, keyword=None): return "aapen" return "ukjent" -def send_report(changes, warnings): - if not changes and not warnings: return +def send_report(changes, warnings, successes): + if not changes and not warnings and not successes: return subject = f"TeeOff Banestatus Rapport - {datetime.now().strftime('%d.%m.%Y')}" body = "BANESTATUS RAPPORT\n" + "="*30 + "\n\n" - if changes: body += "✅ OPPDATERINGER:\n" + "\n".join(changes) + "\n\n" - if warnings: body += "⚠️ MERKNADER / ADVARSLER:\n" + "\n".join(warnings) + "\n" - msg = MIMEMultipart(); msg['From'] = os.getenv("SMTP_USER"); msg['To'] = os.getenv("EMAIL_TO"); msg['Subject'] = subject + if changes: body += "✅ OPPDATERINGER:\n" + "\n".join(changes) + "\n\n" + if warnings: body += "⚠️ MERKNADER / ADVARSLER:\n" + "\n".join(warnings) + "\n\n" + if successes: body += "🆗 VELLYKKEDE SJEKKER (INGEN ENDRING):\n" + "\n".join(successes) + "\n" + + msg = MIMEMultipart() + msg['From'] = os.getenv("SMTP_USER") + msg['To'] = os.getenv("EMAIL_TO") + msg['Subject'] = subject msg.attach(MIMEText(body, 'plain')) try: with smtplib.SMTP_SSL(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT"))) as server: server.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASS")) server.send_message(msg) print("✅ Rapport sendt på e-post.") - except Exception as e: print(f"❌ E-post feil: {e}") + except Exception as e: + print(f"❌ E-post feil: {e}") async def run_daily_scraping(): print(f"🚀 Starter sjekk {datetime.now().strftime('%H:%M:%S')}...") conn = await asyncpg.connect(DB_URL) - facilities = await conn.fetch("SELECT id, name, scrape_status_url, scrape_status_selector FROM facilities WHERE scrape_status_url IS NOT NULL") + facilities = await conn.fetch("SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method FROM facilities WHERE scrape_status_url IS NOT NULL") - changes, warnings = [], [] + changes, warnings, successes = [], [], [] async with async_playwright() as p: browser = await p.chromium.launch(headless=True) @@ -87,17 +85,32 @@ async def run_daily_scraping(): try: print(f"🔍 Besøker {f['name']}...") - await page.goto(f['scrape_status_url'], timeout=60000, wait_until="networkidle") - - # Vent på at innholdet skal lande - await asyncio.sleep(3) + # Endret fra networkidle til domcontentloaded for å unngå Arendal-timeout + await page.goto(f['scrape_status_url'], timeout=60000, wait_until="domcontentloaded") + await asyncio.sleep(3) # Gir Javascript 3 sekunder på å bygge siden - element = await page.query_selector(f['scrape_status_selector']) - if not element: - warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}'") - continue + full_text = "" + method = f.get('scrape_method') or 'css_selector' + + if method == 'css_selector': + element = page.locator(f['scrape_status_selector']).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke CSS-elementet '{f['scrape_status_selector']}'") + continue + full_text = await element.inner_text() + + elif method == 'iframe_golfbox': + frame = page.frame_locator('iframe[src*="golfbox"]') + element = frame.locator(f['scrape_status_selector']).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}' i iframen") + continue + full_text = await element.inner_text() - full_text = await element.inner_text() + else: + warnings.append(f"⚠️ {f['name']}: Ukjent skrapemetode i databasen: '{method}'") + continue + await conn.execute("UPDATE facilities SET status_updated_at = CURRENT_DATE WHERE id = $1", f['id']) courses = await conn.fetch("SELECT id, name, status, scrape_keyword FROM courses WHERE facility_id = $1", f['id']) @@ -105,7 +118,7 @@ async def run_daily_scraping(): new_status = interpret_status(full_text, c['scrape_keyword']) if new_status == "NOT_FOUND": - warnings.append(f"❓ {f['name']} ({c['name']}): Fant ikke søkeordet '{c['scrape_keyword']}' på siden.") + warnings.append(f"❓ {f['name']} ({c['name']}): Fant ikke søkeordet '{c['scrape_keyword']}' i teksten på siden.") continue old_status = c['status'] or "ukjent" @@ -114,16 +127,19 @@ async def run_daily_scraping(): changes.append(f"🔹 {f['name']} ({c['name']}): {old_status.upper()} ➔ {new_status.upper()}") print(f"✅ Oppdatert status for {f['name']} - {c['name']}") else: + successes.append(f"✅ {f['name']} ({c['name']}): {new_status.upper()}") print(f" - {c['name']}: Ingen endring ({new_status.upper()})") except Exception as e: - warnings.append(f"🔥 {f['name']}: Feil: {str(e)[:100]}") + # Trekker ut kun første linje av feilmeldingen for å unngå massiv og stygg tekst i e-posten + err_msg = str(e).split('\n')[0] + warnings.append(f"🔥 {f['name']}: Feil under skraping: {err_msg}") finally: await page.close() await browser.close() await conn.close() - send_report(changes, warnings) + send_report(changes, warnings, successes) print("🏁 Ferdig.") if __name__ == "__main__": diff --git a/frontend/src/app/admin/login/page.tsx b/frontend/src/app/admin/login/page.tsx index a3587fb..9f19401 100644 --- a/frontend/src/app/admin/login/page.tsx +++ b/frontend/src/app/admin/login/page.tsx @@ -39,6 +39,7 @@ export default function AdminLogin() { setError(data.detail || 'Ugyldig pålogging'); } } catch (err) { + console.error("🔥 DEN EKTE FEILEN ER:", err); setError('Systemfeil: Kunne ikke koble til API-et'); } finally { setIsLoading(false);