Table of Contents

Namespace UsluzionicaServer.Infrastructure.Media

Classes

CloudVisionImageModerator

Provera slika preko Google Cloud Vision SafeSearch-a.

ZAŠTO GOOGLE, a ne lokalni ONNX model: Prvih 1.000 slika mesečno je besplatno — za aplikaciju koja tek kreće to je oko 200 novih oglasa sa po pet slika. Lokalni model (NsfwSharp, NudeNet) ne bi koštao ništa po slici, ali bi na jeftinom Hetzner VPS-u trošio CPU koji deli sa bazom i API-jem, i nosio bi ~50 MB modela u Docker slici.

ZAŠTO REST, a ne Google.Cloud.Vision.V1 paket: Paket povlači gRPC i ceo Google.Api lanac zavisnosti zbog jednog poziva koji je običan POST sa base64 telom. Ovako nema nove zavisnosti.

KLJUČ NE SME U REPO. Čita se iz konfiguracije (ImageModeration:ApiKey), koja na produkciji dolazi iz .env-a.

ImageUploads

Provera otpremljenih slika po SADRŽAJU fajla, i dodela ekstenzije na osnovu onoga što je stvarno pronađeno.

Ranije se radilo dvoje, i oboje je bilo pogrešno:

  1. Format se proveravao kroz file.ContentType — a to je header koji klijent piše kako hoće. Nije bila kontrola nego uljudna molba.

  2. Ekstenzija se uzimala iz file.FileName, dakle isto iz korisničkog unosa, i lepila na ime fajla koji završava u wwwroot. Pošto wwwroot servira statični middleware, upload sa imenom payload.html i header-om image/jpeg je pravio fajl koji se servira kao text/html — dakle stored XSS na domenu API-ja. Isto sa .svg, koji sme da nosi <script>.

Ovde se ekstenzija NIKAD ne uzima od klijenta. Čita se prvih nekoliko bajtova, utvrdi format, i vrati se ekstenzija koju MI biramo. Ime fajla koje je klijent poslao se ne koristi ni za šta.

Uz to se sadržaj pretražuje za tragovima skripte (vidi ContainsScriptMarkerAsync(Stream, byte[], CancellationToken)), jer validan JPEG može u sebi nositi i drugi payload — klasično je dopisati ga IZA završnog markera slike, gde ga dekoder ignoriše a drugi parser ne mora.

Ni to nije garancija: obfuskacija i kodiranje zaobilaze svaku pretragu po obrascu. Jedina potpuna mera je re-enkodiranje slike, koje uništava sve što nije piksel. Zato klijent svaku sliku pretvara u JPEG pre slanja (UsluzionicaApp/Services/ImageConverter.cs), a ovo je odbrana za slučaj da neko zaobiđe klijenta i gađa API direktno.

MediaUrlJsonModifier

Pretvara relativne putanje slika u pune URL-ove u TRENUTKU SERIJALIZACIJE.

Zašto ovako, a ne u svakom mapperu: postoji preko 20 mesta u servisima koja pune *ImageUrl polja DTO-ova (ListingService, ProviderService, ReviewService, ConversationService, BookingService, FavoriteService, UserService, ChatHub). Ručna izmena svakog bi značila da jedno propušteno mesto tiho vraća neupotrebljiv URL, i da svaki novi DTO mora da se seti istog koraka.

Ovako pravilo živi na jednom mestu i važi automatski za svako string polje čije se ime završava na "ImageUrl" — i za sve buduće DTO-ove.

Transformacija je JEDNOSMERNA (samo pisanje/odgovor). Deserijalizacija zahteva ostaje netaknuta, pa klijent koji slučajno pošalje pun URL nazad neće upisati domen u bazu — za to se stara MediaUrls.ToRelative na upisu.

MediaUrls

Slike se u bazi čuvaju kao RELATIVNE putanje ("/uploads/listings/3/abc.jpg").

Ranije se upisivao pun URL sa domenom, pa bi svaka promena domena (localhost → produkcija, ili kasnije CDN) polomila sve postojeće slike i zahtevala migraciju podataka. Relativna putanja je prenosiva.

Pun URL se sastavlja tek pri serijalizaciji odgovora — vidi MediaUrlJsonModifier.

NoopImageModerator

Ne proverava ništa — sve propušta.

Podrazumevana implementacija: u razvoju i u testovima automatska provera nema šta da radi, a Cloud Vision bi tražio ključ i trošio kvotu na izmišljene slike.

Bira se konfiguracijom (ImageModeration:Provider), pa je uključivanje prave provere na produkciji samo pitanje podešavanja.

Interfaces

IImageModerator

Automatska provera otpremljenih slika.

TRI NIVOA, NE DVA — i to je za Uslužionicu suština, ne detalj.

U bazi već postoje kategorije „Depilacija voskom", „Depilacija šećerom", „Fitnes trener" i „Plivanje — instruktor". Legitimna fotografija u tim kategorijama redovno sadrži golu kožu: leđa, noge, stomak, osoba u kupaćem na bazenu. Binarni filter „ima kože → odbij" blokirao bi tačne oglase, korisnik ne bi dobio objašnjenje i ne bi znao šta da promeni — dakle napravio bi veći problem nego što rešava.

Zato srednji nivo: sumnjiva slika prolazi, a prijava ide čoveku.

FAIL-OPEN. Ako provera ne uspe — mreža, istek vremena, greška servisa — vraća se Review, nikad Reject. Pad tuđeg servisa ne sme da obori postavljanje oglasa. Isti princip kao AbortOnConnectFail=false kod Redisa.

Enums

ImageVerdict

Ishod automatske provere slike.