Tweak admin permissions and photo view counts

* Profile pictures on profile pages now link to the gallery when clicked.
* Admins can no longer automatically see the default profile pic on profile
  pages unless they have photo moderator ability.
* Photo view counts are not added when an admin with photo moderator ability
  should not have otherwise been able to see the photo.
This commit is contained in:
Noah Petherbridge 2024-09-28 12:45:20 -07:00
parent 3fdae1d8d7
commit f2e847922f
8 changed files with 44 additions and 29 deletions

View File

@ -72,9 +72,9 @@ func Profile() http.HandlerFunc {
// Inject relationship booleans for profile picture display. // Inject relationship booleans for profile picture display.
models.SetUserRelationships(currentUser, []*models.User{user}) models.SetUserRelationships(currentUser, []*models.User{user})
// Admin user can always see the profile pic - but only on this page. Other avatar displays // Admin user (photo moderator) can always see the profile pic - but only on this page.
// will show the yellow or pink shy.png if the admin is not friends or not granted. // Other avatar displays will show the yellow or pink shy.png if the admin is not friends or not granted.
if currentUser.IsAdmin { if currentUser.HasAdminScope(config.ScopePhotoModerator) {
user.UserRelationship.IsFriend = true user.UserRelationship.IsFriend = true
user.UserRelationship.IsPrivateGranted = true user.UserRelationship.IsPrivateGranted = true
} }

View File

@ -98,20 +98,7 @@ func Likes() http.HandlerFunc {
if user, err := models.GetUser(photo.UserID); err == nil { if user, err := models.GetUser(photo.UserID); err == nil {
// Safety check: if the current user should not see this picture, they can not "Like" it. // Safety check: if the current user should not see this picture, they can not "Like" it.
// Example: you unfriended them but they still had the image on their old browser page. // Example: you unfriended them but they still had the image on their old browser page.
var unallowed bool if ok, _ := photo.ShouldBeSeenBy(currentUser); !ok {
if currentUser.ID != user.ID {
if (photo.Visibility == models.PhotoFriends && !models.AreFriends(user.ID, currentUser.ID)) ||
(photo.Visibility == models.PhotoPrivate && !models.IsPrivateUnlocked(user.ID, currentUser.ID)) {
unallowed = true
}
}
// Blocking safety check: if either user blocks the other, liking is not allowed.
if models.IsBlocking(currentUser.ID, user.ID) {
unallowed = true
}
if unallowed {
SendJSON(w, http.StatusForbidden, Response{ SendJSON(w, http.StatusForbidden, Response{
Error: "You are not allowed to like that photo.", Error: "You are not allowed to like that photo.",
}) })
@ -121,7 +108,7 @@ func Likes() http.HandlerFunc {
// Mark this photo as 'viewed' if it received a like. // Mark this photo as 'viewed' if it received a like.
// Example: on a gallery view the photo is only 'viewed' if interacted with (lightbox), // Example: on a gallery view the photo is only 'viewed' if interacted with (lightbox),
// going straight for the 'Like' button should count as well. // going straight for the 'Like' button should count as well.
photo.View(currentUser.ID) photo.View(currentUser)
targetUser = user targetUser = user
} }

View File

@ -49,7 +49,7 @@ func ViewPhoto() http.HandlerFunc {
} }
// Check permission to have seen this photo. // Check permission to have seen this photo.
if ok, err := photo.CanBeSeenBy(currentUser); !ok { if ok, err := photo.ShouldBeSeenBy(currentUser); !ok {
log.Error("Photo %d can't be seen by %s: %s", photo.ID, currentUser.Username, err) log.Error("Photo %d can't be seen by %s: %s", photo.ID, currentUser.Username, err)
SendJSON(w, http.StatusNotFound, Response{ SendJSON(w, http.StatusNotFound, Response{
Error: "Photo Not Found", Error: "Photo Not Found",
@ -58,7 +58,7 @@ func ViewPhoto() http.HandlerFunc {
} }
// Mark a view. // Mark a view.
if err := photo.View(currentUser.ID); err != nil { if err := photo.View(currentUser); err != nil {
log.Error("Update photo(%d) views: %s", photo.ID, err) log.Error("Update photo(%d) views: %s", photo.ID, err)
} }

View File

@ -85,7 +85,7 @@ func View() http.HandlerFunc {
_, isSubscribed := models.IsSubscribed(currentUser, "photos", photo.ID) _, isSubscribed := models.IsSubscribed(currentUser, "photos", photo.ID)
// Mark this photo as "Viewed" by the user. // Mark this photo as "Viewed" by the user.
if err := photo.View(currentUser.ID); err != nil { if err := photo.View(currentUser); err != nil {
log.Error("Update photo(%d) views: %s", photo.ID, err) log.Error("Update photo(%d) views: %s", photo.ID, err)
} }

View File

@ -109,8 +109,27 @@ func GetPhotos(IDs []uint64) (map[uint64]*Photo, error) {
// CanBeSeenBy checks whether a photo can be seen by the current user. // CanBeSeenBy checks whether a photo can be seen by the current user.
// //
// An admin user with omni photo view permission can always see the photo.
//
// Note: this function incurs several DB queries to look up the photo's owner and block lists. // Note: this function incurs several DB queries to look up the photo's owner and block lists.
func (p *Photo) CanBeSeenBy(currentUser *User) (bool, error) { func (p *Photo) CanBeSeenBy(currentUser *User) (bool, error) {
// Admins with photo moderator ability can always see.
if currentUser.HasAdminScope(config.ScopePhotoModerator) {
return true, nil
}
return p.ShouldBeSeenBy(currentUser)
}
// ShouldBeSeenBy checks whether a photo should be seen by the current user.
//
// Even if the current user is an admin with photo moderator ability, this function will return
// whether the admin 'should' be able to see if not for their admin status. Example: a private
// photo may be shown to the admin so they can moderate it, but they shouldn't be able to "like"
// it or mark it as "viewed."
//
// Note: this function incurs several DB queries to look up the photo's owner and block lists.
func (p *Photo) ShouldBeSeenBy(currentUser *User) (bool, error) {
// Find the photo's owner. // Find the photo's owner.
user, err := GetUser(p.UserID) user, err := GetUser(p.UserID)
if err != nil { if err != nil {
@ -120,7 +139,7 @@ func (p *Photo) CanBeSeenBy(currentUser *User) (bool, error) {
var isOwnPhoto = currentUser.ID == user.ID var isOwnPhoto = currentUser.ID == user.ID
// Is either one blocking? // Is either one blocking?
if !currentUser.IsAdmin && IsBlocking(currentUser.ID, user.ID) { if IsBlocking(currentUser.ID, user.ID) {
return false, errors.New("is blocking") return false, errors.New("is blocking")
} }
@ -129,13 +148,13 @@ func (p *Photo) CanBeSeenBy(currentUser *User) (bool, error) {
areFriends = AreFriends(user.ID, currentUser.ID) areFriends = AreFriends(user.ID, currentUser.ID)
isPrivate = user.Visibility == UserVisibilityPrivate && !areFriends isPrivate = user.Visibility == UserVisibilityPrivate && !areFriends
) )
if isPrivate && !currentUser.IsAdmin && !isOwnPhoto { if isPrivate && !isOwnPhoto {
return false, errors.New("user is private and we aren't friends") return false, errors.New("user is private and we aren't friends")
} }
// Is this a private photo and are we allowed to see? // Is this a private photo and are we allowed to see?
isGranted := IsPrivateUnlocked(user.ID, currentUser.ID) isGranted := IsPrivateUnlocked(user.ID, currentUser.ID)
if p.Visibility == PhotoPrivate && !isGranted && !isOwnPhoto && !currentUser.IsAdmin { if p.Visibility == PhotoPrivate && !isGranted && !isOwnPhoto {
return false, errors.New("photo is private") return false, errors.New("photo is private")
} }
@ -193,14 +212,19 @@ func PaginateUserPhotos(userID uint64, conf UserGallery, pager *Pagination) ([]*
// View a photo, incrementing its Views count but not its UpdatedAt. // View a photo, incrementing its Views count but not its UpdatedAt.
// Debounced with a Redis key. // Debounced with a Redis key.
func (p *Photo) View(userID uint64) error { func (p *Photo) View(user *User) error {
// The owner of the photo does not count views. // The owner of the photo does not count views.
if p.UserID == userID { if p.UserID == user.ID {
return nil return nil
} }
// Should the viewer be able to see this, regardless of their admin ability?
if ok, err := p.ShouldBeSeenBy(user); !ok {
return err
}
// Debounce this. // Debounce this.
var redisKey = fmt.Sprintf(config.PhotoViewDebounceRedisKey, userID, p.ID) var redisKey = fmt.Sprintf(config.PhotoViewDebounceRedisKey, user.ID, p.ID)
if redis.Exists(redisKey) { if redis.Exists(redisKey) {
return nil return nil
} }

View File

@ -42,6 +42,8 @@ func SetUserRelationships(currentUser *User, users []*User) error {
// Inject the UserRelationships. // Inject the UserRelationships.
for _, u := range users { for _, u := range users {
u.UserRelationship.Computed = true
if u.ID == currentUser.ID { if u.ID == currentUser.ID {
// Current user - set both bools to true - you can always see your own profile pic. // Current user - set both bools to true - you can always see your own profile pic.
u.UserRelationship.IsFriend = true u.UserRelationship.IsFriend = true

View File

@ -11,7 +11,9 @@
{{if or (not .CurrentUser) .IsExternalView}} {{if or (not .CurrentUser) .IsExternalView}}
<img src="{{.User.VisibleAvatarURL nil}}" data-photo-id="{{.User.ProfilePhoto.ID}}"> <img src="{{.User.VisibleAvatarURL nil}}" data-photo-id="{{.User.ProfilePhoto.ID}}">
{{else}} {{else}}
<img src="{{.User.VisibleAvatarURL .CurrentUser}}" data-photo-id="{{.User.ProfilePhoto.ID}}"> <a href="/u/{{.User.Username}}/photos">
<img src="{{.User.VisibleAvatarURL .CurrentUser}}" data-photo-id="{{.User.ProfilePhoto.ID}}">
</a>
{{end}} {{end}}
<!-- CurrentUser can upload a new profile pic --> <!-- CurrentUser can upload a new profile pic -->

View File

@ -804,7 +804,7 @@
}).then(response => response.json()) }).then(response => response.json())
.then(data => { .then(data => {
if (data.StatusCode !== 200) { if (data.StatusCode !== 200) {
window.alert(data.data.error); console.error("When marking photo %d as viewed: status code %d: %s", photoID, data.StatusCode, data.data.error);
return; return;
} }
}).catch(window.alert); }).catch(window.alert);