diff --git a/go.mod b/go.mod index f5fea9d..7d7dd19 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/artem-streltsov/url-shortener go 1.20 require ( + github.com/davidmytton/url-verifier v1.0.1 github.com/google/safebrowsing v0.0.0-20190624211811-bbf0d20d26b3 github.com/gorilla/sessions v1.2.1 github.com/joho/godotenv v1.5.1 @@ -11,6 +12,7 @@ require ( ) require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/golang/protobuf v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect diff --git a/go.sum b/go.sum index 1878084..8875984 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,8 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davidmytton/url-verifier v1.0.1 h1:eTSdMo5v0HtvrFObYInmt/WTmy5Izlh5gAa0AtrUzKc= +github.com/davidmytton/url-verifier v1.0.1/go.mod h1:kha47HNj0Zg0cozShEaIEPmT3nn7c8N1TGnh8U2B4jc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -24,6 +29,7 @@ github.com/rakyll/statik v0.1.5/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= @@ -41,6 +47,7 @@ golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index f635a85..c89cd81 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -102,7 +102,20 @@ func (h *Handler) newURLHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - err := h.templates.ExecuteTemplate(w, "new.html", nil) + flashes := session.Flashes("error") + var errorMsg string + if len(flashes) > 0 { + errorMsg, _ = flashes[0].(string) + } + session.Save(r, w) + + data := struct { + Error string + }{ + Error: errorMsg, + } + + err := h.templates.ExecuteTemplate(w, "new.html", data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -110,7 +123,9 @@ func (h *Handler) newURLHandler(w http.ResponseWriter, r *http.Request) { case http.MethodPost: err := r.ParseForm() if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + session.AddFlash("Error parsing form", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } @@ -118,23 +133,32 @@ func (h *Handler) newURLHandler(w http.ResponseWriter, r *http.Request) { password := r.Form.Get("password") if url == "" { - http.Error(w, "URL is required", http.StatusBadRequest) + session.AddFlash("URL is required", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } - if !utils.IsValidURL(url) { - http.Error(w, "Invalid URL", http.StatusBadRequest) + url, isValid := utils.IsValidURL(url) + if !isValid { + session.AddFlash("Invalid URL", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } isSafe, err := safebrowsing.IsSafeURL(url) if err != nil { - http.Error(w, "Error checking URL safety", http.StatusInternalServerError) + session.AddFlash("The provided URL is not safe", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } if !isSafe { - http.Error(w, "The provided URL is not safe", http.StatusBadRequest) + session.AddFlash("The provided URL is not safe", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } @@ -144,17 +168,23 @@ func (h *Handler) newURLHandler(w http.ResponseWriter, r *http.Request) { if password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - http.Error(w, "Error hashing password", http.StatusInternalServerError) + session.AddFlash("Error hashing password", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } hashedPassword = string(hash) } if err := h.db.InsertURL(url, key, user.ID, hashedPassword); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + session.AddFlash("Error inserting URL into database", "error") + session.Save(r, w) + http.Redirect(w, r, "/new", http.StatusSeeOther) return } + session.AddFlash("URL successfully added", "success") + session.Save(r, w) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -343,6 +373,22 @@ func (h *Handler) dashboardHandler(w http.ResponseWriter, r *http.Request) { return } + errorFlashes := session.Flashes("error") + var errorMsg string + if len(errorFlashes) > 0 { + errorMsg, _ = errorFlashes[0].(string) + } + + var successMsg string + if errorMsg == "" { + successFlashes := session.Flashes("success") + if len(successFlashes) > 0 { + successMsg, _ = successFlashes[0].(string) + } + } + + session.Save(r, w) + urls, err := h.db.GetURLsByUserID(user.ID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -350,13 +396,17 @@ func (h *Handler) dashboardHandler(w http.ResponseWriter, r *http.Request) { } data := struct { - User *database.User - URLs []database.URL - Host string + User *database.User + URLs []database.URL + Host string + Success string + Error string }{ - User: user, - URLs: urls, - Host: r.Host, + User: user, + URLs: urls, + Host: r.Host, + Success: successMsg, + Error: errorMsg, } err = h.templates.ExecuteTemplate(w, "dashboard.html", data) @@ -376,30 +426,46 @@ func (h *Handler) editURLHandler(w http.ResponseWriter, r *http.Request) { urlID, err := strconv.ParseInt(strings.TrimPrefix(r.URL.Path, "/edit/"), 10, 64) if err != nil { - http.Error(w, "Invalid URL ID", http.StatusBadRequest) + session.AddFlash("Invalid URL ID", "error") + session.Save(r, w) + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) return } url, err := h.db.GetURLByID(urlID) if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) + session.AddFlash("URL not found", "error") + session.Save(r, w) + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) return } if url.UserID != user.ID { - http.Error(w, "Unauthorized", http.StatusForbidden) + session.AddFlash("Unauthorized access", "error") + session.Save(r, w) + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) return } switch r.Method { case http.MethodGet: + flashes := session.Flashes("error") + var errorMsg string + if len(flashes) > 0 { + errorMsg, _ = flashes[0].(string) + } + session.Save(r, w) + data := struct { - URL *database.URL - Host string + URL *database.URL + Host string + Error string }{ - URL: url, - Host: r.Host, + URL: url, + Host: r.Host, + Error: errorMsg, } + err := h.templates.ExecuteTemplate(w, "edit.html", data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -410,23 +476,32 @@ func (h *Handler) editURLHandler(w http.ResponseWriter, r *http.Request) { newPassword := r.FormValue("password") if newURL == "" { - http.Error(w, "URL is required", http.StatusBadRequest) + session.AddFlash("URL is required", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } - if !utils.IsValidURL(newURL) { - http.Error(w, "Invalid URL", http.StatusBadRequest) + newURL, isValid := utils.IsValidURL(newURL) + if !isValid { + session.AddFlash("Invalid URL provided", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } isSafe, err := safebrowsing.IsSafeURL(newURL) if err != nil { - http.Error(w, "Error checking URL safety", http.StatusInternalServerError) + session.AddFlash("Error checking URL safety", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } if !isSafe { - http.Error(w, "The provided URL is not safe", http.StatusBadRequest) + session.AddFlash("The provided URL is not safe", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } @@ -434,7 +509,9 @@ func (h *Handler) editURLHandler(w http.ResponseWriter, r *http.Request) { if newPassword != "" { hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { - http.Error(w, "Error hashing password", http.StatusInternalServerError) + session.AddFlash("Error hashing password", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } hashedPassword = string(hash) @@ -442,10 +519,14 @@ func (h *Handler) editURLHandler(w http.ResponseWriter, r *http.Request) { err = h.db.UpdateURL(urlID, newURL, hashedPassword) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + session.AddFlash("Error updating the URL", "error") + session.Save(r, w) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) return } + session.AddFlash("URL updated successfully", "success") + session.Save(r, w) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) diff --git a/internal/templates/dashboard.html b/internal/templates/dashboard.html index 197bd6a..eb6d637 100644 --- a/internal/templates/dashboard.html +++ b/internal/templates/dashboard.html @@ -39,6 +39,12 @@