TODO_pregled_koda: ažuriran sažetak posle ispravki, označene potvrde i preostale niže-prioritetne stavke

This commit is contained in:
2026-07-31 03:05:02 +02:00
parent 3998119744
commit fc7e17b085
+31 -20
View File
@@ -20,7 +20,7 @@ Legenda ozbiljnosti: 🔴 visok · 🟡 srednji · 🟢 nizak
Postojeća konvencija za ovakav slučaj već postoji u repo-u: `data-full-reload` (v. `nabavka_detalji.html:197`, `pdv_kir.html:29`, `kpo.html:24` itd.) — forma bi trebalo da dobije taj atribut da se isključi iz generičkog handlera, ili se custom `@submit.prevent` mehanizam treba ukloniti u korist generičkog.
**Preporuka**: potvrditi u browseru (Network tab) da li se zaista šalju 2 zahteva pri klik na "Naplati", pa dodati `data-full-reload` na `<form>` (ili ukloniti dupli mehanizam).
- [ ] 🟢 Ostale forme (`nabavka_forma.html`, `servis_forma.html`, itd.) ne koriste `@submit` Alpine direktivu (potvrđeno: `grep -rln "@submit" web/templates/` vraća samo `prodaja_forma.html`) — generički AJAX handler u `base.html` je jedini submit-put za njih, nema konflikta. Bez akcije.
- [x] 🟢 Ostale forme (`nabavka_forma.html`, `servis_forma.html`, itd.) ne koriste `@submit` Alpine direktivu (potvrđeno: `grep -rln "@submit" web/templates/` vraća samo `prodaja_forma.html`) — generički AJAX handler u `base.html` je jedini submit-put za njih, nema konflikta. Bez akcije.
---
@@ -28,23 +28,24 @@ Legenda ozbiljnosti: 🔴 visok · 🟡 srednji · 🟢 nizak
- [x]**`prodaja_forma.html:131,223``pdv_stopa[]` bez `:disabled` — VEĆ ISPRAVLJENO** u prethodnom zadatku (dodato `:disabled="isMobile"` / `:disabled="!isMobile"` analogno susednim poljima).
- [ ] 🟢 `nabavka_forma.html` (jedina druga stranica sa `isMobile` dual-render obrascem, `stavke` blok) — svi `[]` inputi (`artikal_id[]`, `kolicina[]`, `cena_po_komadu[]`, `marza[]`, `prodajna[]`) imaju simetričan `:disabled="isMobile"`/`:disabled="!isMobile"` par (redovi 135-161 desktop, 208-233 mobilni). **Nema iste greške.**
- [x] 🟢 `nabavka_forma.html` (jedina druga stranica sa `isMobile` dual-render obrascem, `stavke` blok) — svi `[]` inputi (`artikal_id[]`, `kolicina[]`, `cena_po_komadu[]`, `marza[]`, `prodajna[]`) imaju simetričan `:disabled="isMobile"`/`:disabled="!isMobile"` par (redovi 135-161 desktop, 208-233 mobilni). **Nema iste greške.**
Zavisni troškovi (`trosak_naziv[]`, `trosak_iznos[]`, redovi 274-278) NISU dual-render (samo jedan `x-for` blok, bez posebne mobilne kopije) — nema rizika duplog slanja.
- [ ] 🟢 Nijedna druga `stranice/*.html` stranica ne koristi `isMobile` dual-render Alpine obrazac (servis, klijenti, artikli/magacin liste koriste responsive CSS/HTMX pristup, ne duplo renderovanje `x-for` sa `:disabled` prekidačem) — obrazac bug-a je specifičan za prodaju/nabavku, oba pokrivena.
- [x] 🟢 Nijedna druga `stranice/*.html` stranica ne koristi `isMobile` dual-render Alpine obrazac (servis, klijenti, artikli/magacin liste koriste responsive CSS/HTMX pristup, ne duplo renderovanje `x-for` sa `:disabled` prekidačem) — obrazac bug-a je specifičan za prodaju/nabavku, oba pokrivena.
---
## 3. Repository sloj — transakciona ispravnost i race conditions
- [ ] Svih **20** `BeginTx(...)` poziva u `internal/db/sqlite/*.go` ima `defer tx.Rollback()` odmah posle provere greške (provereno u: `artikal.go`, `rezervni_kodovi.go`, `servisni_potrazivani_delovi.go`, `nabavka.go`, `prodaja.go`, `nivelacija.go`, `servisni_radovi.go`, `servisni_delovi.go`, `servis.go`) — **nema propusta**, bez akcije.
- [x] Svih **20** `BeginTx(...)` poziva u `internal/db/sqlite/*.go` ima `defer tx.Rollback()` odmah posle provere greške (provereno u: `artikal.go`, `rezervni_kodovi.go`, `servisni_potrazivani_delovi.go`, `nabavka.go`, `prodaja.go`, `nivelacija.go`, `servisni_radovi.go`, `servisni_delovi.go`, `servis.go`) — **nema propusta**, bez akcije.
- [x] 🟡 **`ServisRepo.Kreiraj` nema idempotency zaštitu — ISPRAVLJENO.**
Primenjen isti obrazac kao za prodaju (migracija 106): nova migracija `migrations/107_servis_idempotency_key.sql` (kolona `idempotency_key` + parcijalni `UNIQUE` indeks na `servisni_nalozi`), `model.ServisniNalog.IdempotencyKey` polje, `ServisRepo.Kreiraj` (`internal/db/sqlite/servis.go`) proverava postojeći ključ pre insert-a i vraća postojeći ID ako je nalog već kreiran istim ključem, `parseFormuNaloga` (`internal/handler/servis.go`) čita `idempotency_key` iz forme, `servis_forma.html` dobija skriveno polje + JS UUID generator (samo za novi nalog, ne za izmenu). Dodat `internal/db/sqlite/servis_idempotency_test.go` (2 testa: sa ključem vraća isti ID, bez ključa prave se dva odvojena naloga kao ranije) — oba prolaze.
- [ ] 🟢 `NabavkaRepo.Kreiraj` (`internal/db/sqlite/nabavka.go:178`) nema interni sekvencijalni broj (koristi `broj_racuna` dobavljača, korisnički unet) — nema race-a oko generisanja broja, ali isti opšti rizik "dva odvojena POST-a = dve nabavke" postoji arhitekturno kao i svuda gde nema idempotency ključa. Niži prioritet jer nema poznatu žalbu/simptom.
- [x] 🟢 `NabavkaRepo.Kreiraj` (`internal/db/sqlite/nabavka.go:178`) nema interni sekvencijalni broj (koristi `broj_racuna` dobavljača, korisnički unet) — nema race-a oko generisanja broja, ali isti opšti rizik "dva odvojena POST-a = dve nabavke" postoji arhitekturno kao i svuda gde nema idempotency ključa. Niži prioritet jer nema poznatu žalbu/simptom.
**PREOSTALO — nije automatski rađeno.** Isti obrazac (idempotency_key kolona + parcijalni UNIQUE indeks) bi se mogao primeniti i ovde radi pune konzistentnosti sa prodajom/servisom — javljeno koordinatoru na odluku, nije prioritet jer nema poznat simptom.
- [ ] 🟢 Parametrizacija SQL upita — nasumičan pregled `fmt.Sprintf`/string-concat obrazaca u `internal/db/sqlite/*.go` (magacin.go:112, artikal.go:167,191, klijent.go, trosak.go, usluga.go) — svi slučajevi concat-uju samo **hardkodovane** identifikatore kolona/tabela (const liste, fiksni literali iz poziva) ili grade `?` placeholder nizove; korisnički unos ide isključivo kroz `args`. **Nema SQL injection rizika** u pregledanim mestima.
- [x] 🟢 Parametrizacija SQL upita — nasumičan pregled `fmt.Sprintf`/string-concat obrazaca u `internal/db/sqlite/*.go` (magacin.go:112, artikal.go:167,191, klijent.go, trosak.go, usluga.go) — svi slučajevi concat-uju samo **hardkodovane** identifikatore kolona/tabela (const liste, fiksni literali iz poziva) ili grade `?` placeholder nizove; korisnički unos ide isključivo kroz `args`. **Nema SQL injection rizika** u pregledanim mestima.
---
@@ -59,40 +60,50 @@ Legenda ozbiljnosti: 🔴 visok · 🟡 srednji · 🟢 nizak
Lista (`filter.KorisnikID = &k.ID`, red 51) i kreiranje ISPRAVNO vezuju podsetnik za korisnika (osim kad admin/superadmin eksplicitno dodeli drugom korisniku, red 260-269), ali IZMENA/ZAVRŠI/BRISANJE nemaju tu proveru — bilo koji ulogovan korisnik (uključujući najniže privilegovanu ulogu) može menjati/brisati/označavati kao završene tuđe (pa i admin/superadmin) podsetnike prostim pogađanjem/iteracijom numeričkog ID-a u POST ruti.
**Preporuka**: u sve tri funkcije dohvatiti postojeći podsetnik, proveriti `postojeci.KorisnikID == &k.ID` (ili `k.Uloga` je admin/superadmin), vratiti 403/redirect sa greškom ako ne odgovara — isti obrazac kao provera vlasništva koja bi trebalo da postoji za bilo koji "moj" resurs.
- [ ] 🟢 CSRF pokrivenost rutera (`cmd/ntech/main.go`) — sve mutating rute (`POST/PUT/DELETE`) su unutar jednog od dva `r.Group` bloka koji uključuju `ntechmw.CsrfMiddleware` (redovi 255-263 javne, 285-515 zaštićene), OSIM `/status/{token}/prihvati|odbij|odluka-odabrano` (redovi 270-281) — ovo je **namerno i dokumentovano** (komentar red 265-268): autentikacija je jednokratni tajni token u URL-u, capability model, ne sesijski kolačić, pa klasičan CSRF model napada ne važi. Logika je zdrava, bez akcije, samo zabeleženo radi potpunosti pregleda.
- [x] 🟢 CSRF pokrivenost rutera (`cmd/ntech/main.go`) — sve mutating rute (`POST/PUT/DELETE`) su unutar jednog od dva `r.Group` bloka koji uključuju `ntechmw.CsrfMiddleware` (redovi 255-263 javne, 285-515 zaštićene), OSIM `/status/{token}/prihvati|odbij|odluka-odabrano` (redovi 270-281) — ovo je **namerno i dokumentovano** (komentar red 265-268): autentikacija je jednokratni tajni token u URL-u, capability model, ne sesijski kolačić, pa klasičan CSRF model napada ne važi. Logika je zdrava, bez akcije, samo zabeleženo radi potpunosti pregleda.
- [ ] 🟢 Raw Go greške korisniku — pregled `http.Error(w, err.Error(), ...)` / sličnih obrazaca u `internal/handler/*.go`: **nije pronađen nijedan slučaj** curenja sirove Go greške korisniku; svuda se koriste fiksne srpske poruke uz `slog.Error` za internu dijagnostiku. Bez akcije.
- [x] 🟢 Raw Go greške korisniku — pregled `http.Error(w, err.Error(), ...)` / sličnih obrazaca u `internal/handler/*.go`: **nije pronađen nijedan slučaj** curenja sirove Go greške korisniku; svuda se koriste fiksne srpske poruke uz `slog.Error` za internu dijagnostiku. Bez akcije.
- [ ] 🟢 Sistematski `_ = ...`/`x, _ = ...` obrasci u `internal/handler/*.go` (npr. `podsetnici.go:91,154,287``korisnici, _ = h.KorisniciRepo.Lista(...)` za popunu dropdown-a; `prijava.go` — best-effort audit logovanje neuspešnih pokušaja prijave) — namerno "best-effort" ponašanje (log/dropdown ne sme blokirati glavni tok), prihvatljivo. Jedini blagi rizik: ako `KorisniciRepo.Lista` padne, dropdown za dodelu podsetnika drugom korisniku će tiho biti prazan bez indikacije greške korisniku — kozmetički, nizak prioritet.
- [x] 🟢 Sistematski `_ = ...`/`x, _ = ...` obrasci u `internal/handler/*.go` (npr. `podsetnici.go:91,154,287``korisnici, _ = h.KorisniciRepo.Lista(...)` za popunu dropdown-a; `prijava.go` — best-effort audit logovanje neuspešnih pokušaja prijave) — namerno "best-effort" ponašanje (log/dropdown ne sme blokirati glavni tok), prihvatljivo. Jedini blagi rizik: ako `KorisniciRepo.Lista` padne, dropdown za dodelu podsetnika drugom korisniku će tiho biti prazan bez indikacije greške korisniku — kozmetički, nizak prioritet.
**PREOSTALO — nije automatski rađeno.** Kozmetička izmena (npr. `slog.Warn` pri grešci) bi se mogla dodati — javljeno koordinatoru na odluku, vrlo nizak prioritet.
---
## 5. Neslaganja model ↔ baza ↔ šablon
- [ ] 🟢 Spot-provera `model.ProdajniNalog``SELECT` u `ProdajaRepo.DohvatiID` (`internal/db/sqlite/prodaja.go:121-147`) — polja se poklapaju 1:1 (uklj. i novo `IdempotencyKey`, namerno izostavljeno iz SELECT-a jer se ne prikazuje nigde — ne predstavlja bug). Nije pronađeno neslaganje na ovom mestu.
- [x] 🟢 Spot-provera `model.ProdajniNalog``SELECT` u `ProdajaRepo.DohvatiID` (`internal/db/sqlite/prodaja.go:121-147`) — polja se poklapaju 1:1 (uklj. i novo `IdempotencyKey`, namerno izostavljeno iz SELECT-a jer se ne prikazuje nigde — ne predstavlja bug). Nije pronađeno neslaganje na ovom mestu.
Dublja/iscrpna provera SVIH modul-tabela-šablon trojki nije urađena u ovom prolazu zbog obima repoa (~40+ tabela) — vredi ponoviti ciljano po modulu ako se posumnja na konkretno polje.
---
## 6. Ostalo
- [ ] 🟢 Nema zaostalih `TODO`/`FIXME`/`XXX`/`HACK` komentara u `internal/`, `web/`, `cmd/` (pretraga bez pogotka) — kod je već "čist" po tom kriterijumu.
- [ ] 🟢 Data-refresh re-izvršavanje `<script>` blokova (`base.html:437-441`) — ugrađeni inline skriptovi unutar `data-refresh` regiona (npr. `servis_detalji.html` naplata-dijalog skripta oko reda 100-234) su ispravno vezani za NOVI DOM čvor pri svakom `replaceWith`, pa ne dolazi do akumulacije listenera na istom, trajnom elementu — obrazac je ispravan, bez akcije.
- [x] 🟢 Nema zaostalih `TODO`/`FIXME`/`XXX`/`HACK` komentara u `internal/`, `web/`, `cmd/` (pretraga bez pogotka) — kod je već "čist" po tom kriterijumu.
- [x] 🟢 Data-refresh re-izvršavanje `<script>` blokova (`base.html:437-441`) — ugrađeni inline skriptovi unutar `data-refresh` regiona (npr. `servis_detalji.html` naplata-dijalog skripta oko reda 100-234) su ispravno vezani za NOVI DOM čvor pri svakom `replaceWith`, pa ne dolazi do akumulacije listenera na istom, trajnom elementu — obrazac je ispravan, bez akcije.
---
## Sažetak
Status posle ispravke: sva 🔴 visok i 🟡 srednji rizik nalaza su ISPRAVLJENA (3/3). Svi 🟢 nizak
rizik nalazi su ili potvrde "bez akcije" (kod je već ispravan) ili su prijavljeni koordinatoru
kao preostali, po dogovoru bez automatske izmene.
| Kategorija | Nalaza | 🔴 Visok | 🟡 Srednji | 🟢 Nizak |
|---|---|---|---|---|
| 1. Dual-submit / listeneri | 2 | 1 | 0 | 1 |
| 2. Dual-render forme | 3 | 0 | 0 | 3 (1 već ispravljen) |
| 3. Repository / transakcije | 4 | 0 | 1 | 3 |
| 4. Handler / CSRF / dozvole | 4 | 1 | 0 | 3 |
| 1. Dual-submit / listeneri | 2 | 1 | 0 | 1 |
| 2. Dual-render forme | 3 | 0 | 0 | 3 (1 ispravljen ranije) |
| 3. Repository / transakcije | 4 | 0 | 1 | 3 (1 preostalo) |
| 4. Handler / CSRF / dozvole | 4 | 1 | 0 | 3 (1 preostalo) |
| 5. Model↔baza↔šablon | 1 | 0 | 0 | 1 |
| 6. Ostalo | 2 | 0 | 0 | 2 |
| **Ukupno** | **16** | **2** | **1** | **13** |
| **Ukupno** | **16** | **2/2 ✅** | **1/1 ✅** | **13** (2 preostala, niska prioriteta) |
**Najozbiljniji nalazi (visok rizik):**
1. Mogući dupli POST na `/prodaja/nova` zbog dva nezavisna 'submit' listenera (generički AJAX handler u `base.html` + Alpine `@submit.prevent` u `prodaja_forma.html`) — verovatan pravi uzrok originalno prijavljenog bug-a dupliranja prodaje.
2. IDOR na podsetnicima — izmena/završavanje/brisanje tuđih podsetnika bez provere vlasništva (`internal/handler/podsetnici.go`).
**Ispravljeni nalazi (visok/srednji rizik):**
1. **Dupli POST na `/prodaja/nova`** — dodat `data-full-reload` na formu, isključuje je iz generičkog AJAX interceptora u `base.html`; jedini submit-put ostaje Alpine `@submit.prevent`. Commit: `db1b551`.
2. **IDOR na podsetnicima** — dodata `korisnikSmeDaMenjaPodsetnik` provera vlasništva u `SacuvajIzmenePodsetnika`, `OznaciPodsetnik`, `ObrisiPodsetnik`; 403 ako korisnik nije vlasnik niti admin/superadmin. Commit: `b9e7043`.
3.**`ServisRepo.Kreiraj` bez idempotency zaštite** — dodat isti obrazac kao za prodaju (migracija 107, `idempotency_key` kolona + parcijalni UNIQUE indeks, provera u `Kreiraj`, skriveno polje u `servis_forma.html`). Commit: `3998119`.
**Preostalo (nizak rizik, nije automatski rađeno — čeka odluku):**
- `NabavkaRepo.Kreiraj` nema isti idempotency obrazac (nema poznat simptom, niži prioritet).
- Silent-fail dropdown u `podsetnici.go` (`korisnici, _ = ...`) — kozmetička izmena, vrlo nizak prioritet.