Før endringer fra Ubuntu-laptop
This commit is contained in:
parent
67d1c2ff8c
commit
bacfb51e50
5 changed files with 94 additions and 54 deletions
Binary file not shown.
|
|
@ -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
|
import pyotp
|
||||||
from passlib.hash import pbkdf2_sha256
|
from passlib.hash import pbkdf2_sha256
|
||||||
import getpass
|
import getpass
|
||||||
|
import sys
|
||||||
|
|
||||||
def generate_admin():
|
def generate_admin():
|
||||||
print("\n" + "="*50)
|
print("\n" + "="*50)
|
||||||
print(" TEE OFF ADMIN GENERATOR v1.3")
|
print(" TEE OFF ADMIN GENERATOR v1.9 (DEBUG MODE)")
|
||||||
print("="*50)
|
print("="*50)
|
||||||
|
|
||||||
username = input("Brukernavn (f.eks Envide Webutvikling): ").strip()
|
username = input("Brukernavn (f.eks Envide Webutvikling): ").strip()
|
||||||
email = input("E-post: ").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()
|
otp_secret = pyotp.random_base32()
|
||||||
|
|
||||||
print("⏳ Genererer PBKDF2-hash...")
|
print("⏳ Genererer PBKDF2-hash...")
|
||||||
# Dette vil produsere en streng som starter med $pbkdf2-sha256$...
|
|
||||||
password_hash = pbkdf2_sha256.hash(password)
|
password_hash = pbkdf2_sha256.hash(password)
|
||||||
|
print(f"[DEBUG] Hash generert. Lengde: {len(password_hash)} tegn.")
|
||||||
|
|
||||||
print("\n✅ GENERERING VELLYKKET!")
|
print("\n✅ GENERERING VELLYKKET!")
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
print("1. KJØR DISSE TO KOMMANDOENE I REKKEFØLGE:")
|
print("SLIK LEGGER DU INN BRUKEREN TRYGT:")
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
|
print("1. Gå inn i databasen:")
|
||||||
# Kommando 1: Tøm tabellen for å fjerne gamle, inkompatible hasher
|
print(" docker exec -it teeoff_db psql -U teeoff_admin -d teeoff")
|
||||||
print(f'docker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "TRUNCATE admins;"')
|
print("\n2. Lim inn disse to linjene nøyaktig slik de står:")
|
||||||
|
print("TRUNCATE admins;")
|
||||||
# Kommando 2: Sett inn den nye brukeren
|
print(f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{safe_username}', '{safe_email}', '{password_hash}', '{otp_secret}');")
|
||||||
sql = f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{username}', '{email}', '{password_hash}', '{otp_secret}');"
|
print("\n3. Skriv 'exit' for å gå ut.")
|
||||||
print(f'\ndocker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "{sql}"')
|
print("-" * 50)
|
||||||
|
print("4. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:")
|
||||||
print("\n" + "-" * 50)
|
print(f"Bruk denne nøkkelen: {otp_secret}")
|
||||||
print("2. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:")
|
|
||||||
print(f"Nøkkel: {otp_secret}")
|
|
||||||
print("-" * 50 + "\n")
|
print("-" * 50 + "\n")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
generate_admin()
|
generate_admin()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nAvbrutt.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
@ -126,7 +126,7 @@ app.add_middleware(
|
||||||
@app.post("/api/auth/login")
|
@app.post("/api/auth/login")
|
||||||
async def login(data: dict):
|
async def login(data: dict):
|
||||||
"""Steg 1: Sjekk passord og returner temp_token for 2FA."""
|
"""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:
|
async with app.state.pool.acquire() as conn:
|
||||||
admin = await conn.fetchrow(
|
admin = await conn.fetchrow(
|
||||||
|
|
@ -138,17 +138,20 @@ async def login(data: dict):
|
||||||
print(" - ❌ Bruker ikke funnet i databasen")
|
print(" - ❌ Bruker ikke funnet i databasen")
|
||||||
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
|
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
|
||||||
|
|
||||||
# DEBUG: Printer starten på hashen for å verifisere formatet ($pbkdf2-sha256$...)
|
|
||||||
h = admin['password_hash']
|
h = admin['password_hash']
|
||||||
print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)")
|
print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)")
|
||||||
|
|
||||||
|
# FIKS: Vi pakker KUN selve verify-sjekken inn i try/except
|
||||||
try:
|
try:
|
||||||
if not pwd_context.verify(data.get('password'), h):
|
is_valid = pwd_context.verify(data.get('password'), h)
|
||||||
|
except Exception as 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")
|
print(" - ❌ Passordet samsvarer ikke med hashen")
|
||||||
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
|
raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord")
|
||||||
except Exception as e:
|
|
||||||
print(f" - 🔥 FEIL VED VERIFISERING: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail="Internt problem med passord-format")
|
|
||||||
|
|
||||||
temp_token = jwt.encode(
|
temp_token = jwt.encode(
|
||||||
{"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)},
|
{"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import os
|
import os
|
||||||
import asyncpg
|
import asyncpg
|
||||||
import smtplib
|
import smtplib
|
||||||
import re # Ny import for tekst-vasking
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
@ -12,7 +12,6 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from playwright_stealth import stealth as apply_stealth
|
from playwright_stealth import stealth as apply_stealth
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
@ -20,28 +19,21 @@ load_dotenv()
|
||||||
DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff"
|
DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff"
|
||||||
|
|
||||||
def clean_text(text):
|
def clean_text(text):
|
||||||
"""Fjerner spesialtegn og normaliserer tekst for sammenligning"""
|
|
||||||
return re.sub(r'[^a-zA-Z0-9æøåÆØÅ]', '', text).lower()
|
return re.sub(r'[^a-zA-Z0-9æøåÆØÅ]', '', text).lower()
|
||||||
|
|
||||||
def interpret_status(text, keyword=None):
|
def interpret_status(text, keyword=None):
|
||||||
t_raw = text.lower()
|
t_raw = text.lower()
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
# Fuzzy match: Vi renser både tekst og søkeord for bindestreker/mellomrom
|
|
||||||
k_clean = clean_text(keyword)
|
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):
|
if k_clean not in clean_text(t_raw):
|
||||||
return "NOT_FOUND"
|
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)
|
parts = re.split(re.escape(keyword), t_raw, flags=re.IGNORECASE)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
t_raw = parts[1][:150]
|
t_raw = parts[1][:150]
|
||||||
else:
|
else:
|
||||||
# Fallback hvis keywordet er delt av HTML-tagger (f.eks 18 <strong>hull</strong>)
|
t_raw = t_raw[-200:]
|
||||||
t_raw = t_raw[-200:] # Bruk slutten av teksten hvis ordet er vanskelig å isolere
|
|
||||||
|
|
||||||
if any(word in t_raw for word in ["stengt", "lukket", "frost", "snø", "is", "closed", "stenger"]):
|
if any(word in t_raw for word in ["stengt", "lukket", "frost", "snø", "is", "closed", "stenger"]):
|
||||||
return "stengt"
|
return "stengt"
|
||||||
|
|
@ -53,28 +45,34 @@ def interpret_status(text, keyword=None):
|
||||||
return "aapen"
|
return "aapen"
|
||||||
return "ukjent"
|
return "ukjent"
|
||||||
|
|
||||||
def send_report(changes, warnings):
|
def send_report(changes, warnings, successes):
|
||||||
if not changes and not warnings: return
|
if not changes and not warnings and not successes: return
|
||||||
subject = f"TeeOff Banestatus Rapport - {datetime.now().strftime('%d.%m.%Y')}"
|
subject = f"TeeOff Banestatus Rapport - {datetime.now().strftime('%d.%m.%Y')}"
|
||||||
body = "BANESTATUS RAPPORT\n" + "="*30 + "\n\n"
|
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'))
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP_SSL(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT"))) as server:
|
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.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASS"))
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
print("✅ Rapport sendt på e-post.")
|
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():
|
async def run_daily_scraping():
|
||||||
print(f"🚀 Starter sjekk {datetime.now().strftime('%H:%M:%S')}...")
|
print(f"🚀 Starter sjekk {datetime.now().strftime('%H:%M:%S')}...")
|
||||||
conn = await asyncpg.connect(DB_URL)
|
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:
|
async with async_playwright() as p:
|
||||||
browser = await p.chromium.launch(headless=True)
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
|
@ -87,17 +85,32 @@ async def run_daily_scraping():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"🔍 Besøker {f['name']}...")
|
print(f"🔍 Besøker {f['name']}...")
|
||||||
await page.goto(f['scrape_status_url'], timeout=60000, wait_until="networkidle")
|
# 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
|
||||||
|
|
||||||
# Vent på at innholdet skal lande
|
full_text = ""
|
||||||
await asyncio.sleep(3)
|
method = f.get('scrape_method') or 'css_selector'
|
||||||
|
|
||||||
element = await page.query_selector(f['scrape_status_selector'])
|
if method == 'css_selector':
|
||||||
if not element:
|
element = page.locator(f['scrape_status_selector']).first
|
||||||
warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}'")
|
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()
|
||||||
|
|
||||||
|
else:
|
||||||
|
warnings.append(f"⚠️ {f['name']}: Ukjent skrapemetode i databasen: '{method}'")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
full_text = await element.inner_text()
|
|
||||||
await conn.execute("UPDATE facilities SET status_updated_at = CURRENT_DATE WHERE id = $1", f['id'])
|
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'])
|
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'])
|
new_status = interpret_status(full_text, c['scrape_keyword'])
|
||||||
|
|
||||||
if new_status == "NOT_FOUND":
|
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
|
continue
|
||||||
|
|
||||||
old_status = c['status'] or "ukjent"
|
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()}")
|
changes.append(f"🔹 {f['name']} ({c['name']}): {old_status.upper()} ➔ {new_status.upper()}")
|
||||||
print(f"✅ Oppdatert status for {f['name']} - {c['name']}")
|
print(f"✅ Oppdatert status for {f['name']} - {c['name']}")
|
||||||
else:
|
else:
|
||||||
|
successes.append(f"✅ {f['name']} ({c['name']}): {new_status.upper()}")
|
||||||
print(f" - {c['name']}: Ingen endring ({new_status.upper()})")
|
print(f" - {c['name']}: Ingen endring ({new_status.upper()})")
|
||||||
|
|
||||||
except Exception as e:
|
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:
|
finally:
|
||||||
await page.close()
|
await page.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
|
|
||||||
await conn.close()
|
await conn.close()
|
||||||
send_report(changes, warnings)
|
send_report(changes, warnings, successes)
|
||||||
print("🏁 Ferdig.")
|
print("🏁 Ferdig.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export default function AdminLogin() {
|
||||||
setError(data.detail || 'Ugyldig pålogging');
|
setError(data.detail || 'Ugyldig pålogging');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("🔥 DEN EKTE FEILEN ER:", err);
|
||||||
setError('Systemfeil: Kunne ikke koble til API-et');
|
setError('Systemfeil: Kunne ikke koble til API-et');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue