Tema
/
Interneto svetainių serverio dalies kūrimas GO

Savininko paviršius: užklausos, valdikliai, rodiniai, maršrutai

Keturi failai kortelėse, priklausomybių tvarka: documents.sqldocuments.godocuments.templrouter.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:

  • ViewDocument ir DownloadDocument abu prasideda nuo ownedDocument, kuri paleidžia GetDocumentForOwner ir įrašo 404 esant ErrNoRows. Vienas pagalbininkas, tad savininko patikros nepamirši nei viename maršrute.
  • DownloadDocument atveria failą su Store.Open ir srautu jį siunčia su Content-Disposition: attachment antrašte — originalus failo vardas, ir ASCII, ir UTF-8, pagal RFC 6266.
  • DeleteDocument paleidžia išankstinę patikrą (svetimas → 404, išsiųstas → 422), tada DeleteDraftDocument; n == 1 reiškia, kad tikrai ištrynė, tad failas pašalinamas ir sąrašas atnaujinamas.
  • parseUUID paverč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 sha256sum sutampa su puslapyje rodomu file_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.