"""TIES4080 pääteohjaus 3: kirjautuminen tietokantaa vasten. Tunnukset ja salasanojen tarkisteet ovat Kayttaja-taulussa. Kanta on sama resepti.db kuin db.py-esimerkissä. """ import os import sqlite3 from functools import wraps from flask import Flask, flash, g, redirect, render_template, request, session, url_for from werkzeug.security import check_password_hash app = Flask(__name__) # Salainen avain allekirjoittaa sessioevästeen. Sen tietävä voi väärentää # session, joten avainta ei kirjoiteta lähdekoodiin eikä versionhallintaan. # users.jyu.fi-palvelimella ei voi asettaa omia ympäristömuuttujia, joten avain # luetaan tiedostosta, joka on www-hakemiston ulkopuolella samassa kansiossa # tietokannan kanssa. # # Tiedosto luodaan kerran komennoilla: # python3 -c "import secrets; print(secrets.token_hex(32))" > ~/public_html/hidden/avain.txt # chmod 600 ~/public_html/hidden/avain.txt AVAINTIEDOSTO = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "hidden", "avain.txt") def lue_salainen_avain(polku): try: with open(polku, encoding="UTF-8") as tiedosto: avain = tiedosto.read().strip() except OSError as virhe: raise RuntimeError( f"Salaista avainta ei saada luettua tiedostosta {polku}. " "Luo tiedosto komennolla: " "python3 -c \"import secrets; print(secrets.token_hex(32))\" > avain.txt" ) from virhe if not avain: raise RuntimeError(f"Avaintiedosto {polku} on tyhjä.") return avain app.secret_key = lue_salainen_avain(AVAINTIEDOSTO) app.config.update( SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_SECURE=True, ) KANTA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "hidden", "resepti.db") def kanta(): if "kanta" not in g: g.kanta = sqlite3.connect(KANTA) g.kanta.row_factory = sqlite3.Row g.kanta.execute("PRAGMA foreign_keys = ON") return g.kanta @app.teardown_appcontext def sulje_kanta(poikkeus): yhteys = g.pop("kanta", None) if yhteys is not None: yhteys.close() def auth(f): @wraps(f) def kaaritty(*args, **kwargs): if "kirjautunut" not in session: return redirect(url_for("start"), code=303) return f(*args, **kwargs) return kaaritty def hae_tarkiste(tunnus): """Palauttaa tunnuksen salasanatarkisteen tai None. Sarakkeelle annetaan AS-alias, jolloin tiedetään varmasti, millä nimellä siihen viitataan. """ rivi = kanta().execute(""" SELECT Tarkiste AS tarkiste FROM Kayttaja WHERE Tunnus = :tunnus """, {"tunnus": tunnus}).fetchone() return rivi["tarkiste"] if rivi is not None else None @app.get("/") def start(): return render_template("kirjaudu-db.html") @app.post("/kirjaudu") def kirjaudu(): tunnus = request.form.get("tunnus", "").strip() salasana = request.form.get("salasana", "") try: tarkiste = hae_tarkiste(tunnus) except sqlite3.Error: app.logger.exception("Tietokantaa ei saada auki") flash("Palvelussa on tilapäinen häiriö. Yritä myöhemmin uudelleen.") return redirect(url_for("start"), code=303) # Sama ilmoitus riippumatta siitä, oliko vika tunnuksessa vai salasanassa: # muuten kirjautumissivu kertoo, mitkä tunnukset ovat olemassa. if tarkiste is None or not check_password_hash(tarkiste, salasana): flash("Tunnus tai salasana on väärin.") return redirect(url_for("start"), code=303) session.clear() session["kirjautunut"] = tunnus return redirect(url_for("kirjautunut"), code=303) @app.post("/kirjaudu-ulos") def kirjaudu_ulos(): session.clear() return redirect(url_for("start"), code=303) @app.get("/kirjautunut") @auth def kirjautunut(): return render_template("kirjautunut.html", tunnus=session["kirjautunut"]) if __name__ == "__main__": app.run(debug=True)