From 7f96edf95da08e2d838786ac83cc7c4cdcd558e4 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 21 Aug 2022 17:29:39 -0700 Subject: [PATCH] Private Profiles & Misc Improvements * Add setting to mark profile as "private" * If a profile is private you can't see their profile page or user photo gallery unless you are friends (or admin) * The Site Gallery never shows pictures from private profiles. * Add HTML5 drag/drop upload support for photo gallery. * Suppress SQL logging except in debug mode. * Clean up extra logs. --- cmd/gosocial/main.go | 7 +++- pkg/controller/account/dashboard.go | 2 - pkg/controller/account/profile.go | 13 +++++- pkg/controller/account/settings.go | 6 +++ pkg/controller/photo/user_gallery.go | 13 +++++- pkg/log/log.go | 2 +- pkg/models/photo.go | 5 +++ pkg/models/user.go | 20 +++++++--- pkg/session/session.go | 2 +- web/templates/account/profile.html | 12 +++++- web/templates/account/search.html | 7 +++- web/templates/account/settings.html | 16 ++++++++ web/templates/index.html | 33 ++++++++------- web/templates/photo/gallery.html | 7 +++- web/templates/photo/upload.html | 60 +++++++++++++++++++++++++--- 15 files changed, 163 insertions(+), 42 deletions(-) diff --git a/cmd/gosocial/main.go b/cmd/gosocial/main.go index d1fefa6..fb3f163 100644 --- a/cmd/gosocial/main.go +++ b/cmd/gosocial/main.go @@ -145,8 +145,11 @@ func initdb(c *cli.Context) { // Load the settings.json config.LoadSettings() - var gormcfg = &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), + var gormcfg = &gorm.Config{} + if c.Bool("debug") { + gormcfg = &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + } } // Initialize the database. diff --git a/pkg/controller/account/dashboard.go b/pkg/controller/account/dashboard.go index e83ed66..d529ac9 100644 --- a/pkg/controller/account/dashboard.go +++ b/pkg/controller/account/dashboard.go @@ -3,7 +3,6 @@ package account import ( "net/http" - "git.kirsle.net/apps/gosocial/pkg/log" "git.kirsle.net/apps/gosocial/pkg/templates" ) @@ -11,7 +10,6 @@ import ( func Dashboard() http.HandlerFunc { tmpl := templates.Must("account/dashboard.html") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Error("Dashboard called") if err := tmpl.Execute(w, r, nil); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go index f8cd245..0cb52d9 100644 --- a/pkg/controller/account/profile.go +++ b/pkg/controller/account/profile.go @@ -37,6 +37,8 @@ func Profile() http.HandlerFunc { return } + var isSelf = currentUser.ID == user.ID + // Banned or disabled? Only admin can view then. if user.Status != models.UserStatusActive && !currentUser.IsAdmin { templates.NotFoundPage(w, r) @@ -49,9 +51,16 @@ func Profile() http.HandlerFunc { return } + // Are they friends? And/or is this user private? + var ( + isFriend = models.FriendStatus(currentUser.ID, user.ID) + isPrivate = !currentUser.IsAdmin && !isSelf && user.Visibility == models.UserVisibilityPrivate && isFriend != "approved" + ) + vars := map[string]interface{}{ - "User": user, - "IsFriend": models.FriendStatus(currentUser.ID, user.ID), + "User": user, + "IsFriend": isFriend, + "IsPrivate": isPrivate, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go index c05b060..2d8cadf 100644 --- a/pkg/controller/account/settings.go +++ b/pkg/controller/account/settings.go @@ -93,9 +93,15 @@ func Settings() http.HandlerFunc { case "preferences": var ( explicit = r.PostFormValue("explicit") == "true" + private = r.PostFormValue("private") == "true" ) user.Explicit = explicit + if private { + user.Visibility = models.UserVisibilityPrivate + } else { + user.Visibility = models.UserVisibilityPublic + } if err := user.Save(); err != nil { session.FlashError(w, r, "Failed to save user to database: %s", err) diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index f75b467..5e261e3 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -5,7 +5,6 @@ import ( "regexp" "git.kirsle.net/apps/gosocial/pkg/config" - "git.kirsle.net/apps/gosocial/pkg/log" "git.kirsle.net/apps/gosocial/pkg/models" "git.kirsle.net/apps/gosocial/pkg/session" "git.kirsle.net/apps/gosocial/pkg/templates" @@ -52,6 +51,17 @@ func UserPhotos() http.HandlerFunc { return } + // Is this user private and we're not friends? + var ( + areFriends = models.AreFriends(user.ID, currentUser.ID) + isPrivate = user.Visibility == models.UserVisibilityPrivate && !areFriends + ) + if isPrivate && !currentUser.IsAdmin && !isOwnPhotos { + session.FlashError(w, r, "This user's profile page and photo gallery are private.") + templates.Redirect(w, "/u/"+user.Username) + return + } + // What set of visibilities to query? visibility := []models.PhotoVisibility{models.PhotoPublic} if isOwnPhotos || currentUser.IsAdmin { @@ -73,7 +83,6 @@ func UserPhotos() http.HandlerFunc { Sort: "created_at desc", } pager.ParsePage(r) - log.Error("Pager: %+v", pager) photos, err := models.PaginateUserPhotos(user.ID, visibility, explicit, pager) // Get the count of explicit photos if we are not viewing explicit photos. diff --git a/pkg/log/log.go b/pkg/log/log.go index 6bb7b78..34234c6 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -16,7 +16,7 @@ func init() { Theme: golog.DarkTheme, }) - log.Config.Level = golog.DebugLevel + log.Config.Level = golog.InfoLevel } // SetDebug toggles debug level logging. diff --git a/pkg/models/photo.go b/pkg/models/photo.go index d07fc3d..5ca3460 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -152,6 +152,11 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)", ) + // Exclude private users' photos. + wheres = append(wheres, + "NOT EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND visibility = 'private')", + ) + // Admin view: get ALL PHOTOS on the site, period. if adminView { query = DB diff --git a/pkg/models/user.go b/pkg/models/user.go index d0c5058..e1b102f 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -19,9 +19,9 @@ type User struct { Username string `gorm:"uniqueIndex"` Email string `gorm:"uniqueIndex"` HashedPassword string - IsAdmin bool `gorm:"index"` - Status UserStatus `gorm:"index"` // active, disabled - Visibility string `gorm:"index"` // public, private + IsAdmin bool `gorm:"index"` + Status UserStatus `gorm:"index"` // active, disabled + Visibility UserVisibility `gorm:"index"` // public, private Name *string Birthdate time.Time Certified bool @@ -36,6 +36,13 @@ type User struct { ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"` } +type UserVisibility string + +const ( + UserVisibilityPublic UserVisibility = "public" + UserVisibilityPrivate = "private" +) + // Preload related tables for the user (classmethod). func (u *User) Preload() *gorm.DB { return DB.Preload("ProfileField").Preload("ProfilePhoto") @@ -60,9 +67,10 @@ func CreateUser(username, email, password string) (*User, error) { } u := &User{ - Username: username, - Email: email, - Status: UserStatusActive, + Username: username, + Email: email, + Status: UserStatusActive, + Visibility: UserVisibilityPublic, } if err := u.HashPassword(password); err != nil { diff --git a/pkg/session/session.go b/pkg/session/session.go index a98ac7c..f9669a4 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -56,7 +56,7 @@ func LoadOrNew(r *http.Request) *Session { key := fmt.Sprintf(config.SessionRedisKeyFormat, sess.UUID) err = redis.Get(key, sess) - log.Error("LoadOrNew: raw from Redis: %+v", sess) + // log.Error("LoadOrNew: raw from Redis: %+v", sess) if err != nil { log.Error("session.LoadOrNew: didn't find %s in Redis: %s", err) } diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index f51f2a6..1e046fa 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -29,6 +29,7 @@

{{.User.NameOrUsername}} + {{if eq .User.Visibility "private"}}{{end}}

{{if ne .User.Status "active"}}

@@ -164,6 +165,14 @@

+ {{if .IsPrivate}} +
+
+ This member's profile page is private. You may send them + a friend request, and only if approved, you may then view their profile page and photo gallery. +
+
+ {{else}}
    @@ -352,7 +361,8 @@
{{end}}
- + + {{end}} {{end}} \ No newline at end of file diff --git a/web/templates/account/search.html b/web/templates/account/search.html index 4c1bea6..499aa63 100644 --- a/web/templates/account/search.html +++ b/web/templates/account/search.html @@ -183,7 +183,12 @@

- {{.NameOrUsername}} + + {{.NameOrUsername}} + + {{if eq .Visibility "private"}} + + {{end}}

diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index 6e9d30c..8dc9784 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -219,6 +219,22 @@

+
+ + +

+ If you check this box then only friends who you have approved are able to + see your profile page and gallery. Your gallery photos also will NOT appear + on the Site Gallery page. +

+
+
diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html index 8e93ef5..0f7d8ce 100644 --- a/web/templates/photo/gallery.html +++ b/web/templates/photo/gallery.html @@ -5,7 +5,12 @@ When User Gallery: .User is defined, .IsOwnPhotos may be. --> {{define "title"}} -{{if .IsSiteGallery}}Member Gallery{{else}}Photos of {{.User.Username}}{{end}} + {{if .IsSiteGallery}} + Member Gallery + {{else}} + Photos of {{.User.Username}} + {{if eq .User.Visibility "private"}}{{end}} + {{end}} {{end}} diff --git a/web/templates/photo/upload.html b/web/templates/photo/upload.html index 6281b56..20c4174 100644 --- a/web/templates/photo/upload.html +++ b/web/templates/photo/upload.html @@ -19,6 +19,16 @@ {{ $User := .CurrentUser }} + + + {{if .EditPhoto}}
@@ -347,13 +357,13 @@ $fileName = document.querySelector("#fileName"), $hiddenPreview = document.querySelector("#imagePreview"), $previewBox = document.querySelector("#previewBox"), - $cropField = document.querySelector("#cropCoords"); + $cropField = document.querySelector("#cropCoords"), + $dropArea = document.querySelector("#drop-modal") + $body = document.querySelector("body"); - // Clear the answer in case of page reload. - $cropField.value = ""; - - $file.addEventListener("change", function() { - let file = this.files[0]; + // Common handler for file selection, either via input + // field or drag/drop onto the page. + let onFile = (file) => { $fileName.innerHTML = file.name; // Read the image to show the preview on-page. @@ -411,7 +421,45 @@ } }); reader.readAsDataURL(file); + }; + // Set up drag/drop file upload events. + $body.addEventListener("dragenter", function(e) { + e.preventDefault(); + e.stopPropagation(); + $dropArea.classList.add("is-active"); + }); + $body.addEventListener("dragover", function(e) { + e.preventDefault(); + e.stopPropagation(); + $dropArea.classList.add("is-active"); + }); + $body.addEventListener("dragleave", function(e) { + e.preventDefault(); + e.stopPropagation(); + $dropArea.classList.remove("is-active"); + }); + $body.addEventListener("drop", function(e) { + e.preventDefault(); + e.stopPropagation(); + $dropArea.classList.remove("is-active"); + + // Grab the file. + let dt = e.dataTransfer; + let file = dt.files[0]; + + // Set the file on the input field too. + $file.files = dt.files; + + onFile(file); + }); + + // Clear the answer in case of page reload. + $cropField.value = ""; + + $file.addEventListener("change", function() { + let file = this.files[0]; + onFile(file); }); }); {{end}}