Onthouden met een cookie
Je gastenboek werkt, maar het vraagt bij elk bericht opnieuw je naam en je wachtwoord, ook als je net een bericht hebt geplaatst. Dat komt niet doordat je iets vergeten bent te programmeren: je server kán niet weten dat jij het weer bent. Hier laat je de browser je naam onthouden. Je wachtwoord typ je nog wel; waarom, ontdek je in opdracht 3.
De server herkent je niet
Elk verzoek staat op zichzelf. De browser vraagt een pagina op, Python stuurt een antwoord, en daarna is het contact verbroken. Vraag je een seconde later een tweede pagina op, dan begint alles opnieuw: je server heeft geen idee dat jij dezelfde bezoeker bent als daarnet. Dat is geen gebrek, het is hoe het web werkt.
Wil je toch iets onthouden tussen twee verzoeken, dan moet je die informatie ergens bewaren. Een cookie is daar de eenvoudigste vorm van: een klein stukje tekst dat je meegeeft met je antwoord, en dat de browser vanaf dan bij élk volgend verzoek automatisch terugstuurt.
Een cookie meegeven
Tot nu toe gaf je endpoint alleen inhoud terug. Een antwoord kan ook opdrachten voor de browser bevatten, en set_cookie is er daar een van. Vervang je POST /gastenboek door deze versie. De controle op de lengte uit Server of browser? en die op het wachtwoord uit Inloggen: een wachtwoord controleren blijven bovenaan staan:
@app.post("/gastenboek")
async def gastenboek_opslaan(
naam: str = Form(...),
wachtwoord: str = Form(...),
bericht: str = Form(...),
):
if len(bericht) > 80:
raise HTTPException(status_code=400, detail="Bericht is te lang")
with SqliteDict("gebruikers.db") as gebruikers:
opgeslagen = gebruikers.get(naam)
if opgeslagen != wachtwoord:
raise HTTPException(status_code=401, detail="Naam of wachtwoord klopt niet")
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {"naam": naam, "bericht": bericht}
db.commit()
antwoord = RedirectResponse(url="/berichten", status_code=303)
antwoord.set_cookie(key="naam", value=naam, max_age=60 * 60 * 24 * 30)
return antwoord
- Regel 19:
Je maakt het antwoord nu eerst als variabele, in plaats van het meteen terug te geven. Return je het meteen, dan heb je geen plek om er iets aan toe te voegen.
- Regel 20:
set_cookiezet een cookie op het antwoord: een naam, een waarde, en metmax_agede houdbaarheid in seconden. Hier is dat 30 dagen:60 * 60 * 24 * 30. Laat jemax_ageweg, dan verdwijnt de cookie zodra de bezoeker de browser afsluit. - Regel 21:
Pas nu gaat het antwoord terug, mét de cookie erop.
Alleen de naam gaat in de cookie, nooit het wachtwoord. Een cookie staat leesbaar in de browser, en reist bij elk verzoek mee.
Gebruik je het htmx-formulier uit de htmx-recepten, dan laat je de omleiding weg: maak van je HTMLResponse de variabele antwoord, zet daar set_cookie op en return hem.
De cookie terugkrijgen
Vanaf nu stuurt de browser die cookie mee bij elk verzoek aan jouw server. Je vraagt hem op met Cookie, net zoals je formuliervelden met Form opvraagt. Cookie komt uit fastapi, net als Form. Zet hem daarom in die regel bovenaan je main.py:
from fastapi import Cookie, FastAPI, Form, HTTPException, Request
Vervang daarna je GET /gastenboek, dat tot nu toe een FileResponse gaf:
@app.get("/gastenboek")
async def gastenboek_form(request: Request, naam: str = Cookie(default="")):
return templates.TemplateResponse(request, "gastenboek.html", {"naam": naam})
- Regel 2:
Cookie(default="")haalt de cookienaamuit het verzoek, zoalsForm(...)een formulierveld haalt.default=""bepaalt wat je krijgt als er nog geen cookie is: bij een eerste bezoek een lege tekst.
Je formulier was tot nu toe een vast bestand, static/pages/gastenboek_form.html. Om er een naam in te kunnen zetten moet het een template worden: verplaats het hele bestand naar templates/gastenboek.html, met de <head> en de script-tags die er misschien al in staan, en zet de waarde in het naamveld. Alleen dat veld verandert: value="{{ naam }}" komt erbij. Zonder htmx en JavaScript ziet het formulier er dan zo uit:
<form method="post" action="/gastenboek">
<input type="text" name="naam" value="{{ naam }}" required>
<input type="password" name="wachtwoord" required>
<input type="text" name="bericht" required>
<button type="submit">Verstuur</button>
</form>
Bij een eerste bezoek is {{ naam }} leeg en zie je een leeg veld. Daarna staat je naam er al in; je wachtwoord typ je zelf. Heeft jouw formulier hx-post en hx-target, of een id voor je tekenteller, laat die dan staan.
Testen
- Start je server en schrijf een bericht, met je naam en je wachtwoord
- Ga terug naar
/gastenboek: je naam staat er al, het wachtwoordveld is leeg - Open de ontwikkelaarstools met
F12en zoek in het tabblad App (Application; in Firefox Opslag) bij Cookies de regelhttp://127.0.0.1:8000. Daar staat jouw naam. Heeft je naam een spatie of een letter als ë, dan zet de server er aanhalingstekens omheen of maakt hij van de ë een code als\353; in het formulier staat hij weer zoals je hem typte
Blijft het veld leeg terwijl je wel een bericht hebt verstuurd? Controleer of set_cookie op het antwoord staat dat je ook echt returnt. Meer oplossingen staan op Er gaat iets mis.
Opdrachten
Opdracht 1: Predict - Wie stuurt wat?
Je hebt een cookie met je naam. Je vraagt daarna drie pagina's op: /berichten, /gastenboek en /bericht/bericht_123.
Opdracht 2: Run
Pas je POST-endpoint aan zodat het de naam als cookie meegeeft, verplaats je formulier naar templates/gastenboek.html en lees de cookie uit in je GET-endpoint. Controleer dat je naam blijft staan.
Opdracht 3: Investigate - Verander je eigen cookie
Zoek de cookie op in het tabblad App en dubbelklik op de waarde om hem in een andere naam te veranderen. Herlaad daarna /gastenboek.
Antwoord
In het formulier staat nu de naam die jij zelf in de cookie hebt getypt. Je server neemt hem klakkeloos over.
Hier kan dat weinig kwaad: met de naam van een ander kom je zonder diens wachtwoord niet in het gastenboek. Maar stel dat je het wachtwoord wilt overslaan, en de cookie zou gelden als bewijs dat je bent ingelogd. Dan was iedereen met één dubbelklik iemand anders. Een cookie staat op de computer van de bezoeker, en die kan er alles van maken. Voor een naam die je alleen als gemak invult is dat prima; voor iets waar rechten aan hangen, zoals wie is ingelogd of wie mag verwijderen, niet. Daar gaat de volgende les over.
Opdracht 4: Make - Vergeet mij
Maak een endpoint /vergeet dat de cookie weghaalt en de bezoeker terugstuurt naar het formulier. Zet er een link naartoe onder je formulier.
Tip
Naast set_cookie bestaat er ook delete_cookie, met alleen de naam van de cookie als argument. Net als bij set_cookie maak je eerst het antwoord, haal je daar de cookie af en return je het.
Antwoord
@app.get("/vergeet")
async def vergeet():
antwoord = RedirectResponse(url="/gastenboek")
antwoord.delete_cookie("naam")
return antwoord
<a href="/vergeet">Ik ben iemand anders</a>
Na het klikken is het naamveld weer leeg: de browser heeft de cookie weggegooid en stuurt hem dus niet meer mee.
Dit mag een GET zijn, want op de server verandert er niets: alleen een cookie voor het gemak verdwijnt uit de browser. Bij een sessie is uitloggen wel een POST, want dan verandert er iets op de server. Dat bouw je later, in opdracht 5 van Cookie of sessie?, en uitgebreider in Uitloggen in de route Veiligheid.
Opdracht 5: Make - Welkom terug
Laat boven het formulier "Welkom terug, [naam]" staan als de bezoeker al eens een bericht heeft geplaatst, en "Welkom" als dat niet zo is.
Tip
De template weet al of naam gevuld is. Met {% if naam %} kun je daarop reageren, net zoals je in Nog niets? if en else in je template een lege lijst opving.
Antwoord
{% if naam %}
<h1>Welkom terug, {{ naam }}</h1>
{% else %}
<h1>Welkom</h1>
{% endif %}
Je endpoint hoeft niet te veranderen: de lege standaardwaarde uit Cookie(default="") is voor Jinja2 al genoeg om te weten dat er niets is.