website/pkg/controller/admin/user_actions.go
Noah Petherbridge 542d0bb300 Improvements on community flagged explicit photos
When a user marks that another photo should have been marked as explicit:

* The owner of that photo gets a notification about it, which reminds them of
  the explicit photo policy.
* The photo's "Flagged" boolean is set (along with the Explicit boolean)
* The 'Edit' page on a Flagged photo shows a red banner above the Explicit
  option, explaining that it was flagged. The checkbox text is crossed-out,
  with a "no" cursor and title text over - but can still be unchecked.

If the user removes the Explicit flag on a flagged photo and saves it:

* An admin report is generated to notify to take a look too.
* The Explicit flag is cleared as normal
* The Flagged boolean is also cleared on this photo: if they set it back to
  Explicit again themselves, the red banner won't appear and it won't notify
  again - unless a community member flagged it again!

Also makes some improvements to the admin page:

* On photo reports: show a blurred-out (clickable to reveal) photo on feedback
  items about photos.
2024-10-01 20:44:11 -07:00

350 lines
10 KiB
Go

package admin
import (
"fmt"
"net/http"
"strconv"
"strings"
"code.nonshy.com/nonshy/website/pkg/chat"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/models/deletion"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// Mark a user photo as Explicit for them.
func MarkPhotoExplicit() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
photoID uint64
next = r.FormValue("next")
)
if !strings.HasPrefix(next, "/") {
next = "/"
}
// Get current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Failed to get current user: %s", err)
templates.Redirect(w, "/")
return
}
if idInt, err := strconv.Atoi(r.FormValue("photo_id")); err == nil {
photoID = uint64(idInt)
} else {
session.FlashError(w, r, "Invalid or missing photo_id parameter: %s", err)
templates.Redirect(w, next)
return
}
// Get this photo.
photo, err := models.GetPhoto(photoID)
if err != nil {
session.FlashError(w, r, "Didn't find photo ID in database: %s", err)
templates.Redirect(w, next)
return
}
photo.Explicit = true
photo.Flagged = true
if err := photo.Save(); err != nil {
session.FlashError(w, r, "Couldn't save photo: %s", err)
} else {
session.Flash(w, r, "Marked photo as Explicit!")
}
// Log the change.
models.LogUpdated(&models.User{ID: photo.UserID}, currentUser, "photos", photo.ID, "Marked explicit by admin action.", []models.FieldDiff{
models.NewFieldDiff("Explicit", false, true),
})
templates.Redirect(w, next)
})
}
// Admin actions against a user account.
func UserActions() http.HandlerFunc {
tmpl := templates.Must("admin/user_actions.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
intent = r.FormValue("intent")
confirm = r.Method == http.MethodPost
reason = r.FormValue("reason") // for impersonation
userId uint64
)
// Get current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Failed to get current user: %s", err)
templates.Redirect(w, "/")
return
}
if idInt, err := strconv.Atoi(r.FormValue("user_id")); err == nil {
userId = uint64(idInt)
} else {
session.FlashError(w, r, "Invalid or missing user_id parameter: %s", err)
templates.Redirect(w, "/admin")
return
}
// Get this user.
user, err := models.GetUser(userId)
if err != nil {
session.FlashError(w, r, "Didn't find user ID in database: %s", err)
templates.Redirect(w, "/admin")
return
}
// Template variables.
var vars = map[string]interface{}{
"Intent": intent,
"User": user,
}
switch intent {
case "insights":
// Admin insights (peek at block lists, etc.)
if !currentUser.HasAdminScope(config.ScopeUserInsight) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserInsight)
templates.Redirect(w, "/admin")
return
}
// Get their block lists.
insights, err := models.GetBlocklistInsights(user)
if err != nil {
session.FlashError(w, r, "Error getting blocklist insights: %s", err)
}
vars["BlocklistInsights"] = insights
// Also surface counts of admin blocks.
count, total := models.CountBlockedAdminUsers(user)
vars["AdminBlockCount"] = count
vars["AdminBlockTotal"] = total
case "chat.rules":
// Chat Moderation Rules.
if !currentUser.HasAdminScope(config.ScopeChatModerator) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeChatModerator)
templates.Redirect(w, "/admin")
return
}
if r.Method == http.MethodPost {
// Rules list for the change log.
var newRules = "(none)"
if rule, ok := r.PostForm["rules"]; ok && len(rule) > 0 {
newRules = strings.Join(rule, ",")
user.SetProfileField("chat_moderation_rules", newRules)
if err := user.Save(); err != nil {
session.FlashError(w, r, "Error saving the user's chat rules: %s", err)
} else {
session.Flash(w, r, "Chat moderation rules have been updated!")
}
} else {
user.DeleteProfileField("chat_moderation_rules")
session.Flash(w, r, "All chat moderation rules have been cleared for user: %s", user.Username)
}
templates.Redirect(w, "/u/"+user.Username)
// Log the new rules to the changelog.
models.LogEvent(
user,
currentUser,
"updated",
"chat.rules",
user.ID,
fmt.Sprintf(
"An admin has updated the chat moderation rules for this user.\n\n"+
"The update rules are: %s",
newRules,
),
)
return
}
vars["ChatModerationRules"] = config.ChatModerationRules
case "essays":
// Edit their profile essays easily.
if !currentUser.HasAdminScope(config.ScopePhotoModerator) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopePhotoModerator)
templates.Redirect(w, "/admin")
return
}
if r.Method == http.MethodPost {
var (
about = r.PostFormValue("about_me")
interests = r.PostFormValue("interests")
musicMovies = r.PostFormValue("music_movies")
)
user.SetProfileField("about_me", about)
user.SetProfileField("interests", interests)
user.SetProfileField("music_movies", musicMovies)
if err := user.Save(); err != nil {
session.FlashError(w, r, "Error saving the user: %s", err)
} else {
session.Flash(w, r, "Their profile text has been updated!")
}
templates.Redirect(w, "/u/"+user.Username)
return
}
case "impersonate":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeUserImpersonate) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserImpersonate)
templates.Redirect(w, "/admin")
return
}
if confirm {
if err := session.ImpersonateUser(w, r, user, currentUser, reason); err != nil {
session.FlashError(w, r, "Failed to impersonate user: %s", err)
} else {
session.Flash(w, r, "You are now impersonating %s", user.Username)
templates.Redirect(w, "/me")
return
}
}
case "ban":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeUserBan) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserBan)
templates.Redirect(w, "/admin")
return
}
if confirm {
status := r.PostFormValue("status")
if status == "active" {
user.Status = models.UserStatusActive
} else if status == "banned" {
user.Status = models.UserStatusBanned
}
user.Save()
session.Flash(w, r, "User ban status updated!")
templates.Redirect(w, "/u/"+user.Username)
// Maybe kick them from chat room now.
if _, err := chat.MaybeDisconnectUser(user); err != nil {
log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err)
}
// Log the change.
models.LogEvent(user, currentUser, models.ChangeLogBanned, "users", currentUser.ID, fmt.Sprintf("User ban status updated to: %s", status))
return
}
case "promote":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeUserPromote) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserPromote)
templates.Redirect(w, "/admin")
return
}
if confirm {
action := r.PostFormValue("action")
user.IsAdmin = action == "promote"
user.Save()
session.Flash(w, r, "User admin status updated!")
templates.Redirect(w, "/u/"+user.Username)
// Log the change.
models.LogEvent(user, currentUser, models.ChangeLogAdmin, "users", currentUser.ID, fmt.Sprintf("User admin status updated to: %s", action))
return
}
case "password":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeUserPassword) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserPassword)
templates.Redirect(w, "/admin")
return
}
if confirm {
password := r.PostFormValue("password")
if len(password) < 3 {
session.FlashError(w, r, "A password of at least 3 characters is required.")
templates.Redirect(w, r.URL.Path+fmt.Sprintf("?intent=password&user_id=%d", user.ID))
return
}
if err := user.SaveNewPassword(password); err != nil {
session.FlashError(w, r, "Failed to set the user's password: %s", err)
} else {
session.Flash(w, r, "The user's password has been updated to: %s", password)
}
templates.Redirect(w, "/u/"+user.Username)
return
}
case "delete":
// Scope check.
if !currentUser.HasAdminScope(config.ScopeUserDelete) {
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserDelete)
templates.Redirect(w, "/admin")
return
}
if confirm {
if err := deletion.DeleteUser(user); err != nil {
session.FlashError(w, r, "Failed when deleting the user: %s", err)
} else {
session.Flash(w, r, "User has been deleted!")
}
templates.Redirect(w, "/admin")
// Kick them from the chat room if they are online.
if _, err := chat.DisconnectUserNow(user, "You have been signed out of chat because your account has been deleted."); err != nil {
log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err)
}
// Log the change.
models.LogDeleted(nil, currentUser, "users", user.ID, fmt.Sprintf("Username %s has been deleted by an admin.", user.Username), nil)
return
}
default:
session.FlashError(w, r, "Unsupported admin user intent: %s", intent)
templates.Redirect(w, "/admin")
return
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// Un-impersonate a user account.
func Unimpersonate() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sess := session.Get(r)
if sess.Impersonator > 0 {
user, err := models.GetUser(sess.Impersonator)
if err != nil {
session.FlashError(w, r, "Couldn't unimpersonate: impersonator (%d) is not an admin!", user.ID)
templates.Redirect(w, "/")
return
}
session.LoginUser(w, r, user)
session.Flash(w, r, "No longer impersonating.")
templates.Redirect(w, "/")
}
templates.Redirect(w, "/")
})
}