Skip to content

Commit

Permalink
better error display and url check
Browse files Browse the repository at this point in the history
  • Loading branch information
artem-streltsov committed Sep 15, 2024
1 parent 0e5f698 commit 6e60167
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 41 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
139 changes: 110 additions & 29 deletions internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,39 +102,63 @@ 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
}
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
}

url := r.Form.Get("url")
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
}

Expand All @@ -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)
Expand Down Expand Up @@ -343,20 +373,40 @@ 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)
return
}

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)
Expand All @@ -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)
Expand All @@ -410,42 +476,57 @@ 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
}

var hashedPassword string
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)
}

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)
Expand Down
8 changes: 7 additions & 1 deletion internal/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ <h1 class="mb-4">Welcome, {{.User.Username}}!</h1>
<a href="/logout" class="btn btn-secondary mb-2 mobile-full-width">Logout</a>
</div>
<h2>Your Shortened URLs</h2>
{{if .Error}}
<div class="alert alert-danger">{{.Error}}</div>
{{end}}
{{if .Success}}
<div class="alert alert-success">{{.Success}}</div>
{{end}}
<div class="desktop-only">
<div class="table-responsive">
<table class="table table-striped">
Expand Down Expand Up @@ -155,4 +161,4 @@ <h5 class="card-title text-truncate">{{.URL}}</h5>
});
</script>
</body>
</html>
</html>
7 changes: 5 additions & 2 deletions internal/templates/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10 col-sm-12">
<h1 class="mb-4">Edit URL</h1>
{{if .Error}}
<div class="alert alert-danger">{{.Error}}</div>
{{end}}
<div class="short-url">
Short URL: <a href="http://{{.Host}}/r/{{.URL.Key}}" target="_blank">http://{{.Host}}/r/{{.URL.Key}}</a>
</div>
<form action="/edit/{{.URL.ID}}" method="POST">
<div class="mb-3">
<label for="url" class="form-label">Original URL</label>
<input type="url" class="form-control" id="url" name="url" value="{{.URL.URL}}" required>
<input type="text" class="form-control" id="url" name="url" value="{{.URL.URL}}" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password (optional)</label>
Expand All @@ -50,4 +53,4 @@ <h1 class="mb-4">Edit URL</h1>
}
</script>
</body>
</html>
</html>
7 changes: 5 additions & 2 deletions internal/templates/new.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<h1 class="text-center mb-4">Create New Short URL</h1>
{{if .Error}}
<div class="alert alert-danger">{{.Error}}</div>
{{end}}
<form action="/new" method="POST">
<div class="mb-3">
<label for="url" class="form-label">URL to shorten</label>
<input type="url" class="form-control" id="url" name="url" required>
<input type="text" class="form-control" id="url" name="url" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password (optional)</label>
Expand All @@ -39,4 +42,4 @@ <h1 class="text-center mb-4">Create New Short URL</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>
Loading

0 comments on commit 6e60167

Please sign in to comment.