Registreren: een account opslaan
Hier bouw je op verder: Formulieren, met een wachtwoordveld
Weet je dit niet meer zeker? Lees het terug voordat je verdergaat.
- Webontwikkeling Formulieren, met een wachtwoordveld
In je gastenboek typt iedereen zelf een naam. Wie "Sara" intypt, is voor je server Sara. Op een echte site maak je daarom eerst een account: een naam die nog vrij is, met een wachtwoord dat alleen jij kent. Nu bewaart je server zo'n account, en in Inloggen: een wachtwoord controleren vraagt je gastenboek er daarna om.
Het formulier
Maak static/pages/registreer.html:
<!DOCTYPE html>
<html>
<body>
<h1>Registreren</h1>
<form method="post" action="/registreer">
<input type="text" name="naam" placeholder="Kies een naam" required>
<input type="password" name="wachtwoord" placeholder="Kies een wachtwoord" required>
<button type="submit">Account maken</button>
</form>
</body>
</html>
Een veld met type="password" toont puntjes in plaats van letters. Dat is alleen voor wie over je schouder meekijkt: het formulier stuurt het wachtwoord net zo leesbaar naar je server als de naam.
Het account opslaan
Een account is een sleutel en een waarde, net als alles in SqliteDict: de naam is de sleutel, het wachtwoord de waarde. Het komt in een eigen database, gebruikers.db, naast gastenboek.db. Zet onder je andere endpoints:
@app.get("/registreer")
async def registreer_formulier():
return FileResponse("static/pages/registreer.html")
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
if naam in gebruikers:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
gebruikers[naam] = wachtwoord
gebruikers.commit()
return RedirectResponse(url="/gastenboek", status_code=303)
- Regel 6:
Twee velden uit het formulier, met dezelfde namen als de
name-attributen. - Regel 7:
De database heet hier
gebruikersin plaats vandb. Zo zie je verderop meteen welke database je bedoelt. - Regel 8-9:
Bestaat de naam al, dan weigert de server met 400 Bad Request: aan je invoer klopt iets niet. Zonder die controle overschrijft wie zich nog een keer als
sararegistreert het wachtwoord van Sara, en is het account voortaan van hem. - Regel 10-11:
Het account opslaan, met
commit(), zoals in Gegevens opslaan met SqliteDict. - Regel 12:
Daarna door naar het gastenboek, met 303, net als na een bericht in Doorsturen na opslaan (redirect).
HTTPException, RedirectResponse en FileResponse staan sinds de vorige lessen al bij je imports.
Testen
- Ga naar
http://127.0.0.1:8000/registreeren maak een accountsaramet het wachtwoordwelkom123. - Je komt uit op het gastenboek. Daar vraagt nog niets om je wachtwoord: dat komt in de volgende les.
- Ga terug naar
/registreeren maak nog een keer een accountsara. Nu staat er{"detail":"Deze naam is al bezet"}, en het tabblad Netwerk toont status400.
Kijk in je database
Je server houdt de terminal bezet. Open er een tweede met het plusje (+) rechtsboven in het terminalpaneel van VS Code. Verander in bekijk_db.py uit Alles bekijken: len en items de bestandsnaam in "gebruikers.db" en draai het daar:
1 sleutels
sara = welkom123
Het wachtwoord staat er zoals Sara het typte. Voor je server is dat handig: bij het inloggen vergelijkt hij straks wat iemand typt met wat hier staat. Maar wie gebruikers.db in handen krijgt, kent zo elk wachtwoord.
Zo bewaar je een wachtwoord op een echte site nooit. Hoe het wel moet, leer je later, in de route Veiligheid: Wachtwoorden veilig opslaan. Gebruik in je gastenboek tot die tijd nooit een wachtwoord dat je ergens anders ook gebruikt.
Er gaat iets mis
422 na Account maken
{"detail":[{"type":"missing","loc":["body","wachtwoord"],"msg":"Field required","input":null}]}
Oorzaak: het formulier stuurt geen veld wachtwoord mee. Meestal heet het veld anders, bijvoorbeeld name="password", terwijl de parameter van je endpoint wachtwoord heet.
Oplossing: maak het name-attribuut in registreer.html letter voor letter gelijk aan de parameter: name="wachtwoord".
Deze naam is al bezet, terwijl je nieuw bent
Oorzaak: gebruikers.db onthoudt elk account, ook na een herstart. Heb je eerder al getest met dezelfde naam, dan bestaat hij al.
Oplossing: kijk met bekijk_db.py welke namen er staan, en kies een andere. Wil je opnieuw beginnen, stop dan je server en gooi gebruikers.db weg.
Zo ziet je main.py er nu uit
Dit is je hele main.py na deze les, zonder wat je in de opdrachten maakte.
import random
import time
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlitedict import SqliteDict
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def home():
return FileResponse("static/pages/home.html")
@app.get("/info")
async def info():
return {"naam": "Jouw Naam", "leeftijd": 16}
@app.get("/pagina", response_class=HTMLResponse)
async def pagina():
return """
<!DOCTYPE html>
<html>
<head>
<title>Mijn Pagina</title>
</head>
<body>
<h1>Hallo!</h1>
<p>Dit is mijn eerste HTML pagina.</p>
</body>
</html>
"""
@app.get("/styled", response_class=HTMLResponse)
async def styled():
return """
<!DOCTYPE html>
<html>
<head>
<title>Met CSS</title>
<style>
h1 { color: blue; }
p { color: gray; }
</style>
</head>
<body>
<h1>Gestylede pagina</h1>
<p>Deze tekst is grijs.</p>
</body>
</html>
"""
@app.get("/about")
async def about():
return FileResponse("static/pages/about.html")
DIEREN = {
"kat": "Een kat spint als hij tevreden is.",
"hond": "Een hond kwispelt als hij blij is.",
"vis": "Een vis ademt met kieuwen.",
}
@app.get("/dier/{naam}", response_class=HTMLResponse)
async def dier(naam: str):
tekst = DIEREN.get(naam, "Dit dier staat nog niet op de site.")
return f"""
<!DOCTYPE html>
<html>
<body>
<h1>Over dieren</h1>
<p>{tekst}</p>
<a href="/">Terug naar Home</a>
</body>
</html>
"""
@app.get("/zoek")
async def zoek(term: str):
return {"je_zocht": term}
@app.get("/foto")
async def foto():
return FileResponse("static/pages/foto.html")
@app.get("/dobbelsteen")
async def dobbelsteen(request: Request):
worp = random.randint(1, 6)
return templates.TemplateResponse(
request,
"dobbelsteen.html",
{"worp": worp}
)
@app.get("/naam")
async def toon_formulier():
return FileResponse("static/pages/naam_form.html")
@app.post("/verstuur")
async def ontvang_naam(naam: str = Form(...)):
print(f"Naam ontvangen: {naam}")
return {"bericht": f"Hallo {naam}!"}
@app.get("/groet")
async def groet_form():
return FileResponse("static/pages/groet_form.html")
@app.post("/groet")
async def groet_verwerk(request: Request, naam: str = Form(...)):
return templates.TemplateResponse(
request,
"groet_resultaat.html",
{"naam": naam}
)
@app.get("/opslaan")
async def naam_formulier():
return FileResponse("static/pages/naam_opslaan.html")
@app.post("/opslaan")
async def naam_opslaan(naam: str = Form(...)):
with SqliteDict("namen.db") as db:
db["laatste_naam"] = naam
db.commit()
return {"bericht": f"'{naam}' opgeslagen!"}
@app.get("/toon_naam")
async def toon_naam():
with SqliteDict("namen.db") as db:
naam = db.get("laatste_naam", "Nog niets opgeslagen")
return {"naam": naam}
@app.get("/gastenboek")
async def gastenboek_form():
return FileResponse("static/pages/gastenboek_form.html")
@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
sleutel = f"bericht_{time.time_ns()}"
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()
return RedirectResponse(url="/berichten", status_code=303)
@app.get("/berichten")
async def berichten(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.items())
return templates.TemplateResponse(
request,
"berichten.html",
{"berichten": alle_berichten}
)
@app.post("/verwijderen")
async def verwijderen(sleutel: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return RedirectResponse(url="/berichten", status_code=303)
@app.get("/bericht/{sleutel}")
async def bericht_detail(request: Request, sleutel: str):
with SqliteDict("gastenboek.db") as db:
bericht = db.get(sleutel)
if bericht is None:
raise HTTPException(status_code=404, detail="Dit bericht bestaat niet")
return templates.TemplateResponse(
request,
"bericht.html",
{"bericht": bericht}
)
@app.get("/registreer")
async def registreer_formulier():
return FileResponse("static/pages/registreer.html")
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
if naam in gebruikers:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
gebruikers[naam] = wachtwoord
gebruikers.commit()
return RedirectResponse(url="/gastenboek", status_code=303)
Opdrachten
Opdracht 1: Predict - Sara en sara
Iemand maakt een account Sara, met een hoofdletter. Daarna maakt iemand anders een account sara.
Opdracht 2: Run
Maak het formulier en de twee endpoints, en registreer twee accounts met verschillende namen. Staan ze allebei in gebruikers.db, met het wachtwoord dat je typte?
Antwoord
Ja. bekijk_db.py toont 2 sleutels, en onder elke naam het wachtwoord. Na een herstart van je server staan ze er nog, want gebruikers.db is een bestand.
Opdracht 3: Investigate - Zonder de controle
Zet een # voor de twee regels met if naam in gebruikers: en raise. Registreer sara met welkom123, en daarna nog een keer sara met overgenomen. Welk wachtwoord staat er nu bij Sara? Haal de # daarna weer weg.
Antwoord
overgenomen. De tweede registratie zet een nieuwe waarde onder dezelfde sleutel, en die overschrijft de oude. Sara kan straks niet meer met haar eigen wachtwoord schrijven, en de ander wel als Sara. Daarom staat de controle vóór het opslaan.
Opdracht 4: Modify - Een langer wachtwoord
Laat /registreer een wachtwoord korter dan 8 tekens weigeren, met status 400 en de melding Kies een wachtwoord van minstens 8 tekens.
Tip
len(wachtwoord) geeft het aantal tekens. Zet de controle vooraan in het endpoint, vóór de database opengaat.
Antwoord
Zet deze regels bovenaan in je registreer, vóór het with-blok:
if len(wachtwoord) < 8:
raise HTTPException(status_code=400, detail="Kies een wachtwoord van minstens 8 tekens")
Het formulier kan dat ook vragen, met minlength="8" op het wachtwoordveld. Dat is handig voor de bezoeker, maar wie het verzoek zelf verstuurt, slaat het formulier over. De controle in het endpoint telt.
Opdracht 5: Make - Wie heeft een account?
Maak een endpoint /leden dat alle namen met een account teruggeeft, zonder hun wachtwoorden: {"leden":["sara","alex"]}.
Tip
keys() geeft alleen de sleutels, zonder de waarden, net als items() je de paren gaf. Maak er binnen het with-blok een lijst van.
Antwoord
@app.get("/leden")
async def leden():
with SqliteDict("gebruikers.db") as gebruikers:
namen = list(gebruikers.keys())
return {"leden": namen}
Geef je hier dict(gebruikers) terug, dan staan de wachtwoorden erbij, voor iedereen die /leden opent. Een endpoint geeft alleen terug wat elke bezoeker mag zien.
Door naar Inloggen: een wachtwoord controleren.