Savininko paviršius: užklausos, valdikliai, rodiniai, maršrutai
Keturi failai kortelėse, priklausomybių tvarka: documents.sql → documents.go → documents.templ → router.go — užklausos, valdikliai, rodiniai, maršrutai.
1. internal/db/queries/documents.sql (keitimas) — dvi naujos užklausos šalia praėjusios pamokos: GetDocumentForOwner (WHERE id = $1 AND owner_id = $2) ir DeleteDraftDocument (WHERE id = $1 AND owner_id = $2 AND status = 'draft', :execrows). Paleisk sqlc generate.
2. internal/handlers/documents.go (keitimas) — trys valdikliai ir du pagalbininkai:
ViewDocumentirDownloadDocumentabu prasideda nuoownedDocument, kuri paleidžiaGetDocumentForOwnerir įrašo 404 esantErrNoRows. Vienas pagalbininkas, tad savininko patikros nepamirši nei viename maršrute.DownloadDocumentatveria failą suStore.Openir srautu jį siunčia suContent-Disposition: attachmentantrašte — originalus failo vardas, ir ASCII, ir UTF-8, pagal RFC 6266.DeleteDocumentpaleidžia išankstinę patikrą (svetimas → 404, išsiųstas → 422), tadaDeleteDraftDocument;n == 1reiškia, kad tikrai ištrynė, tad failas pašalinamas ir sąrašas atnaujinamas.parseUUIDpaverčia{id}kelio parametrą įpgtype.UUID, grąžindama 404 esant netaisyklingam ID, dar prieš pasiekiant duomenų bazę.
func (h *Handlers) ownedDocument(w http.ResponseWriter, r *http.Request) (db.Document, bool) {
user := mustUser(r)
id, ok := parseUUID(chiURLParam(r, "id"))
if !ok {
http.NotFound(w, r)
return db.Document{}, false
}
doc, err := h.Queries.GetDocumentForOwner(r.Context(), db.GetDocumentForOwnerParams{ID: id, OwnerID: user.ID})
if errors.Is(err, pgx.ErrNoRows) {
http.NotFound(w, r) // svetimas ar neegzistuojantis — neatskiriama, tyčia
return db.Document{}, false
}
// ...
return doc, true
}
3. internal/web/documents.templ (keitimas) — sąrašas įgyja veiksmus: failo vardas ir View mygtukas veda į /documents/{id}, o juodraščio eilutė gauna Delete mygtuką (hx-post, hx-confirm, taikosi į #doc-list). Ir naujas DocumentDetail puslapis: metaduomenys, SHA-256, Download mygtukas ir — tik juodraščiui — Delete forma. Trynimo valdikliams nereikia paslėpto CSRF lauko: praėjusios pamokos X-CSRF-Token antraštės sujungimas apima kiekvieną htmx užklausą, įskaitant šią.
4. internal/handlers/router.go (keitimas) — trys maršrutai autentifikuotoje grupėje:
pr.Get("/documents/{id}", h.ViewDocument)
pr.Get("/documents/{id}/download", h.DownloadDocument)
pr.Post("/documents/{id}/delete", h.DeleteDocument)
Patikra — perseeduok, paleisk, prisijunk, įkelk vieną ar du failus, tada:
- Spustelk failo vardą → detalės puslapis rodo tipą, dydį, įkėlimo laiką ir pilną SHA-256.
- Spustelk Download → failas grįžta su originaliu vardu; atsisiųsto failo
sha256sumsutampa su puslapyje rodomufile_hash. - Spustelk Delete ant juodraščio → patvirtink, ir eilutė dingsta sąrašui pasikeičiant vietoje. Terminale užrašoma
document deleted.
Dabar įrodyk dvi saugumo taisykles.
404, ne 403. Paimk dokumento ID iš savo paskyros, tada užsiregistruok antra paskyra ir, kaip tas vartotojas, paprašyk pirmojo vartotojo dokumento. GET nereikia CSRF žetono, tad tai vienaeilutis:
$ curl -si http://localhost:8080/documents/<pirmo-vartotojo-id> \
-H "Cookie: session=<antro-vartotojo-sesija>"
HTTP/1.1 404 Not Found
Antras vartotojas negali pasakyti, ar tas ID egzistuoja — 404 svetimam dokumentui yra tas pats atsakymas kaip 404 netikram.
Trinti tik juodraštį. sent dokumentas nerodo jokio Delete mygtuko — jis už if d.IsDraft. Ir taisyklė galioja serveryje. Ranka perjunk dokumentą į sent, ir trynimas nieko neištrina:
$ psql "$DATABASE_URL" -c "update documents set status='sent' where id='<id>';"
DeleteDraftDocument dabar atitinka nulį eilučių tam ID. Užkomentuok valdiklio if doc.Status != "draft" patikrą, ir jis vis tiek atsisako — nes tikras sargas yra WHERE status = 'draft' užklausoje, ne Go if.