From 71dfa76faa590d7c3297854ea88242fe5fe824ca Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 21 Aug 2022 15:40:24 -0700 Subject: [PATCH] Photo Quotas & Postgres Fixes * Add photo upload quotas. * Non-certified users can upload few photos; certified users more * Fix foreign key issues around deleting user profile photos for psql --- pkg/config/config.go | 4 ++++ pkg/controller/account/login.go | 12 ++++++++++-- pkg/controller/photo/edit_delete.go | 19 +++++++++++++++++-- pkg/controller/photo/upload.go | 12 ++++++++++++ pkg/middleware/authentication.go | 8 ++++---- pkg/models/photo.go | 10 ++++++++++ pkg/models/user.go | 8 +++++++- pkg/photo/quota.go | 22 ++++++++++++++++++++++ pkg/templates/template_funcs.go | 14 ++++++++++++++ web/templates/account/login.html | 1 + web/templates/admin/certification.html | 2 +- web/templates/photo/upload.html | 25 ++++++++++++++++++++++++- 12 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 pkg/photo/quota.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 1794805..36fcc6a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,6 +66,10 @@ var ( const ( MaxPhotoWidth = 1280 ProfilePhotoWidth = 512 + + // Quotas for uploaded photos. + PhotoQuotaUncertified = 6 + PhotoQuotaCertified = 24 ) // Variables set by main.go to make them readily available. diff --git a/pkg/controller/account/login.go b/pkg/controller/account/login.go index bc5d67c..1d92e1d 100644 --- a/pkg/controller/account/login.go +++ b/pkg/controller/account/login.go @@ -16,6 +16,7 @@ import ( func Login() http.HandlerFunc { tmpl := templates.Must("account/login.html") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var next = r.FormValue("next") // Posting? if r.Method == http.MethodPost { @@ -73,11 +74,18 @@ func Login() http.HandlerFunc { // Redirect to their dashboard. session.Flash(w, r, "Login successful.") - templates.Redirect(w, "/me") + if strings.HasPrefix(next, "/") { + templates.Redirect(w, next) + } else { + templates.Redirect(w, "/me") + } return } - if err := tmpl.Execute(w, r, nil); err != nil { + var vars = map[string]interface{}{ + "Next": next, + } + if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/pkg/controller/photo/edit_delete.go b/pkg/controller/photo/edit_delete.go index 2a43b84..bb5ea98 100644 --- a/pkg/controller/photo/edit_delete.go +++ b/pkg/controller/photo/edit_delete.go @@ -1,6 +1,7 @@ package photo import ( + "fmt" "net/http" "strconv" @@ -131,11 +132,15 @@ func Delete() http.HandlerFunc { // Query params. photoID, err := strconv.Atoi(r.FormValue("id")) if err != nil { + log.Error("photo.Delete: failed to parse `id` param (%s) as int: %s", r.FormValue("id"), err) session.FlashError(w, r, "Photo 'id' parameter required.") templates.Redirect(w, "/") return } + // Page to redirect to in case of errors. + redirect := fmt.Sprintf("%s?id=%d", r.URL.Path, photoID) + // Find this photo by ID. photo, err := models.GetPhoto(uint64(photoID)) if err != nil { @@ -162,10 +167,20 @@ func Delete() http.HandlerFunc { confirm := r.PostFormValue("confirm") == "true" if !confirm { session.FlashError(w, r, "Confirm you want to delete this photo.") - templates.Redirect(w, r.URL.Path) + templates.Redirect(w, redirect) return } + // Was this our profile picture? + if currentUser.ProfilePhotoID != nil && *currentUser.ProfilePhotoID == photo.ID { + log.Debug("Delete Photo: was the user's profile photo, unset ProfilePhotoID") + if err := currentUser.RemoveProfilePhoto(); err != nil { + session.FlashError(w, r, "Error unsetting your current profile photo: %s", err) + templates.Redirect(w, redirect) + return + } + } + // Remove the images from disk. for _, filename := range []string{ photo.Filename, @@ -180,7 +195,7 @@ func Delete() http.HandlerFunc { if err := photo.Delete(); err != nil { session.FlashError(w, r, "Couldn't delete photo: %s", err) - templates.Redirect(w, r.URL.Path) + templates.Redirect(w, redirect) return } diff --git a/pkg/controller/photo/upload.go b/pkg/controller/photo/upload.go index 9e98164..e1f013c 100644 --- a/pkg/controller/photo/upload.go +++ b/pkg/controller/photo/upload.go @@ -35,6 +35,11 @@ func Upload() http.HandlerFunc { session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser") } + // Get the current user's quota. + var photoCount, photoQuota = photo.QuotaForUser(user) + vars["PhotoCount"] = photoCount + vars["PhotoQuota"] = photoQuota + // Are they POSTing? if r.Method == http.MethodPost { var ( @@ -47,6 +52,13 @@ func Upload() http.HandlerFunc { confirm2 = r.PostFormValue("confirm2") == "true" ) + // Are they at quota already? + if photoCount >= photoQuota { + session.FlashError(w, r, "You have too many photos to upload a new one. Please delete a photo to make room for a new one.") + templates.Redirect(w, "/photo/u/"+user.Username) + return + } + // They checked both boxes. The browser shouldn't allow them to // post but validate it here anyway... if !confirm1 || !confirm2 { diff --git a/pkg/middleware/authentication.go b/pkg/middleware/authentication.go index 44d02fe..9dd8f4a 100644 --- a/pkg/middleware/authentication.go +++ b/pkg/middleware/authentication.go @@ -21,8 +21,8 @@ func LoginRequired(handler http.Handler) http.Handler { user, err := session.CurrentUser(r) if err != nil { log.Error("LoginRequired: %s", err) - errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden) - errhandler.ServeHTTP(w, r) + session.FlashError(w, r, "You must be signed in to view this page.") + templates.Redirect(w, "/login?next="+r.URL.RawPath) return } @@ -89,8 +89,8 @@ func CertRequired(handler http.Handler) http.Handler { currentUser, err := session.CurrentUser(r) if err != nil { log.Error("LoginRequired: %s", err) - errhandler := templates.MakeErrorPage("Login Required", "You must be signed in to view this page.", http.StatusForbidden) - errhandler.ServeHTTP(w, r) + session.FlashError(w, r, "You must be signed in to view this page.") + templates.Redirect(w, "/login?next="+r.URL.Path) return } diff --git a/pkg/models/photo.go b/pkg/models/photo.go index bc2e910..d07fc3d 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -96,6 +96,16 @@ func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK return p, result.Error } +// CountPhotos returns the total number of photos on a user's account. +func CountPhotos(userID uint64) (int64, error) { + var count int64 + result := DB.Where( + "user_id = ?", + userID, + ).Model(&Photo{}).Count(&count) + return count, result.Error +} + // CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist) func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) { query := DB.Where( diff --git a/pkg/models/user.go b/pkg/models/user.go index 147a82a..d0c5058 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -32,7 +32,7 @@ type User struct { // Relational tables. ProfileField []ProfileField - ProfilePhotoID uint64 + ProfilePhotoID *uint64 ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"` } @@ -329,6 +329,12 @@ func (u *User) ProfileFieldIn(field, substr string) bool { return strings.Contains(value, substr) } +// RemoveProfilePhoto sets profile_photo_id=null to unset the foreign key. +func (u *User) RemoveProfilePhoto() error { + result := DB.Model(&User{}).Where("id = ?", u.ID).Update("profile_photo_id", nil) + return result.Error +} + // Save user. func (u *User) Save() error { result := DB.Save(u) diff --git a/pkg/photo/quota.go b/pkg/photo/quota.go new file mode 100644 index 0000000..97d9b27 --- /dev/null +++ b/pkg/photo/quota.go @@ -0,0 +1,22 @@ +package photo + +import ( + "git.kirsle.net/apps/gosocial/pkg/config" + "git.kirsle.net/apps/gosocial/pkg/models" +) + +// QuoteForUser returns the current photo usage quota for a given user. +func QuotaForUser(u *models.User) (current, allowed int) { + // Count their photos. + count, _ := models.CountPhotos(u.ID) + + // What is their quota at? + var quota int + if !u.Certified { + quota = config.PhotoQuotaUncertified + } else { + quota = config.PhotoQuotaCertified + } + + return int(count), quota +} diff --git a/pkg/templates/template_funcs.go b/pkg/templates/template_funcs.go index 4e3b21a..f280d73 100644 --- a/pkg/templates/template_funcs.go +++ b/pkg/templates/template_funcs.go @@ -41,6 +41,17 @@ func TemplateFuncs(r *http.Request) template.FuncMap { return labels[1] } }, + "Pluralize": func(count int, labels ...string) string { + if len(labels) < 2 { + labels = []string{"", "s"} + } + + if count == 1 { + return labels[0] + } else { + return labels[1] + } + }, "Substring": func(value string, n int) string { if n > len(value) { return value @@ -54,6 +65,9 @@ func TemplateFuncs(r *http.Request) template.FuncMap { } return result }, + "SubtractInt": func(a, b int) int { + return a - b + }, } } diff --git a/web/templates/account/login.html b/web/templates/account/login.html index 13d647c..4fbe481 100644 --- a/web/templates/account/login.html +++ b/web/templates/account/login.html @@ -13,6 +13,7 @@
{{ InputCSRF }} +
diff --git a/web/templates/admin/certification.html b/web/templates/admin/certification.html index 4588bd5..d3604b1 100644 --- a/web/templates/admin/certification.html +++ b/web/templates/admin/certification.html @@ -108,7 +108,7 @@
-

{{.NameOrUsername}}

+

{{$User.NameOrUsername}}

{{$User.Username}} diff --git a/web/templates/photo/upload.html b/web/templates/photo/upload.html index 1e923bf..6281b56 100644 --- a/web/templates/photo/upload.html +++ b/web/templates/photo/upload.html @@ -57,6 +57,28 @@

{{end}} + + {{if not .EditPhoto}} +
+

+ You have currently uploaded {{.PhotoCount}} of your allowed {{.PhotoQuota}} photos. + {{if ge .PhotoCount .PhotoQuota}} + To upload a new photo, please delete + an existing photo first to make room. + {{end}} +

+ +

+ You may upload {{SubtractInt .PhotoQuota .PhotoCount}} more photo{{Pluralize (SubtractInt .PhotoQuota .PhotoCount)}}. + {{if not .CurrentUser.Certified}} + After your account has been certified, you will be able to upload + additional pictures. + {{end}} +

+
+ {{end}} + + {{if or .EditPhoto (lt .PhotoCount .PhotoQuota)}}
@@ -299,7 +321,8 @@
-
+ + {{end}}