package httpapi import ( "errors" "net/http" "strconv" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/company" "github.com/descrybe/descrybe-v2/apps/api/internal/security" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) const brandLogoMaxUpload = 3 << 20 // parse budget slightly above 2 MiB file cap func (s *Server) handleUploadBrandLogo(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) role, _ := RoleFromContext(r.Context()) if role != "admin" { Error(w, http.StatusForbidden, "admin required") return } if err := r.ParseMultipartForm(brandLogoMaxUpload); err != nil { Error(w, http.StatusBadRequest, "invalid multipart form") return } file, header, err := r.FormFile("file") if err != nil { file, header, err = r.FormFile("logo") } if err != nil { Error(w, http.StatusBadRequest, "file field required") return } defer file.Close() logoURL, _, _, _, err := company.SaveBrandLogo( s.Config.UploadDir, cid, header.Filename, header.Header.Get("Content-Type"), file, ) if err != nil { ClientOrLog(w, http.StatusBadRequest, "could not upload logo", err, company.ClientError) return } current, err := company.LoadBrand(r.Context(), s.Pool, cid) if err != nil { Error(w, http.StatusInternalServerError, "load brand failed") return } current.LogoURL = logoURL saved, err := company.UpsertBrand(r.Context(), s.Pool, cid, current) if err != nil { Error(w, http.StatusInternalServerError, "save brand failed") return } s.brandResponse(w, r, saved) } func (s *Server) handleGetBrandLogoFile(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) name := chi.URLParam(r, "filename") s.serveBrandLogo(w, r, cid, name) } func (s *Server) handlePublicBrandLogo(w http.ResponseWriter, r *http.Request) { companyRaw := chi.URLParam(r, "companyID") cid, err := uuid.Parse(companyRaw) if err != nil { Error(w, http.StatusBadRequest, "invalid company id") return } name := chi.URLParam(r, "filename") expRaw := strings.TrimSpace(r.URL.Query().Get("exp")) sig := strings.TrimSpace(r.URL.Query().Get("sig")) exp, err := strconv.ParseInt(expRaw, 10, 64) if err != nil { Error(w, http.StatusForbidden, "invalid or expired signature") return } secret := strings.TrimSpace(s.Config.TokenSigningSecret) if secret == "" { Error(w, http.StatusServiceUnavailable, "signed logos unavailable") return } if err := company.VerifyPublicBrandLogoSig(secret, cid, name, exp, sig); err != nil { Error(w, http.StatusForbidden, "invalid or expired signature") return } s.serveBrandLogo(w, r, cid, name) } func (s *Server) serveBrandLogo(w http.ResponseWriter, r *http.Request, companyID uuid.UUID, name string) { f, contentType, err := company.OpenBrandLogo(s.Config.UploadDir, companyID, name) if err != nil { switch { case errors.Is(err, company.ErrLogoInvalidName), errors.Is(err, company.ErrLogoForbidden): Error(w, http.StatusBadRequest, "invalid logo path") case errors.Is(err, company.ErrLogoNotFound): Error(w, http.StatusNotFound, "logo not found") default: Error(w, http.StatusInternalServerError, "could not open logo") } return } defer f.Close() st, err := f.Stat() if err != nil { Error(w, http.StatusInternalServerError, "could not stat logo") return } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "private, max-age=3600") w.Header().Set("X-Content-Type-Options", "nosniff") http.ServeContent(w, r, name, st.ModTime(), f) } func isBrandLogoURLError(err error) bool { return errors.Is(err, security.ErrInvalidURL) || errors.Is(err, security.ErrBlockedURL) || errors.Is(err, security.ErrBlockedHost) || errors.Is(err, company.ErrLogoInvalidName) }