2022-08-14 23:27:57 +00:00
|
|
|
package admin
|
|
|
|
|
|
|
|
import (
|
2024-02-26 01:28:40 +00:00
|
|
|
"fmt"
|
2022-08-14 23:27:57 +00:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2023-12-27 00:26:23 +00:00
|
|
|
"strings"
|
2022-08-14 23:27:57 +00:00
|
|
|
|
2024-03-15 06:08:14 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/chat"
|
2023-08-02 03:39:48 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
2024-03-15 06:08:14 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
2022-08-26 04:21:46 +00:00
|
|
|
"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"
|
2022-08-14 23:27:57 +00:00
|
|
|
)
|
|
|
|
|
2023-12-27 00:26:23 +00:00
|
|
|
// 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 = "/"
|
|
|
|
}
|
|
|
|
|
Change Logs
* Add a ChangeLog table to collect historic updates to various database tables.
* Created, Updated (with field diffs) and Deleted actions are logged, as well
as certification photo approves/denies.
* Specific items added to the change log:
* When a user photo is marked Explicit by an admin
* When users block/unblock each other
* When photo comments are posted, edited, and deleted
* When forums are created, edited, and deleted
* When forum comments are created, edited and deleted
* When a new forum thread is created
* When a user uploads or removes their own certification photo
* When an admin approves or rejects a certification photo
* When a user uploads, modifies or deletes their gallery photos
* When a friend request is sent
* When a friend request is accepted, ignored, or rejected
* When a friendship is removed
2024-02-26 01:03:36 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-12-27 00:26:23 +00:00
|
|
|
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
|
2024-10-02 03:44:11 +00:00
|
|
|
photo.Flagged = true
|
2023-12-27 00:26:23 +00:00
|
|
|
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!")
|
|
|
|
}
|
Change Logs
* Add a ChangeLog table to collect historic updates to various database tables.
* Created, Updated (with field diffs) and Deleted actions are logged, as well
as certification photo approves/denies.
* Specific items added to the change log:
* When a user photo is marked Explicit by an admin
* When users block/unblock each other
* When photo comments are posted, edited, and deleted
* When forums are created, edited, and deleted
* When forum comments are created, edited and deleted
* When a new forum thread is created
* When a user uploads or removes their own certification photo
* When an admin approves or rejects a certification photo
* When a user uploads, modifies or deletes their gallery photos
* When a friend request is sent
* When a friend request is accepted, ignored, or rejected
* When a friendship is removed
2024-02-26 01:03:36 +00:00
|
|
|
|
|
|
|
// 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),
|
|
|
|
})
|
|
|
|
|
2023-12-27 00:26:23 +00:00
|
|
|
templates.Redirect(w, next)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
// 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
|
2022-12-25 07:00:59 +00:00
|
|
|
reason = r.FormValue("reason") // for impersonation
|
2022-08-14 23:27:57 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-05 03:57:14 +00:00
|
|
|
// Template variables.
|
|
|
|
var vars = map[string]interface{}{
|
|
|
|
"Intent": intent,
|
|
|
|
"User": user,
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
switch intent {
|
2023-12-05 03:57:14 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-09-10 04:17:22 +00:00
|
|
|
// Get their block lists.
|
2023-12-05 03:57:14 +00:00
|
|
|
insights, err := models.GetBlocklistInsights(user)
|
|
|
|
if err != nil {
|
|
|
|
session.FlashError(w, r, "Error getting blocklist insights: %s", err)
|
|
|
|
}
|
|
|
|
vars["BlocklistInsights"] = insights
|
2024-09-10 04:17:22 +00:00
|
|
|
|
|
|
|
// Also surface counts of admin blocks.
|
|
|
|
count, total := models.CountBlockedAdminUsers(user)
|
|
|
|
vars["AdminBlockCount"] = count
|
|
|
|
vars["AdminBlockTotal"] = total
|
2024-09-20 02:30:02 +00:00
|
|
|
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)"
|
2024-09-21 03:32:56 +00:00
|
|
|
if rule, ok := r.PostForm["rules"]; ok && len(rule) > 0 {
|
2024-09-20 02:30:02 +00:00
|
|
|
newRules = strings.Join(rule, ",")
|
|
|
|
user.SetProfileField("chat_moderation_rules", newRules)
|
2024-09-21 03:32:56 +00:00
|
|
|
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!")
|
|
|
|
}
|
2024-09-20 02:30:02 +00:00
|
|
|
} else {
|
|
|
|
user.DeleteProfileField("chat_moderation_rules")
|
2024-09-21 03:32:56 +00:00
|
|
|
session.Flash(w, r, "All chat moderation rules have been cleared for user: %s", user.Username)
|
2024-09-20 02:30:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2024-07-13 19:05:36 +00:00
|
|
|
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
|
|
|
|
}
|
2022-08-14 23:27:57 +00:00
|
|
|
case "impersonate":
|
2023-08-02 03:39:48 +00:00
|
|
|
// Scope check.
|
|
|
|
if !currentUser.HasAdminScope(config.ScopeUserImpersonate) {
|
|
|
|
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserImpersonate)
|
|
|
|
templates.Redirect(w, "/admin")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
if confirm {
|
2022-12-25 07:00:59 +00:00
|
|
|
if err := session.ImpersonateUser(w, r, user, currentUser, reason); err != nil {
|
2022-08-14 23:27:57 +00:00
|
|
|
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":
|
2023-08-02 03:39:48 +00:00
|
|
|
// Scope check.
|
|
|
|
if !currentUser.HasAdminScope(config.ScopeUserBan) {
|
|
|
|
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserBan)
|
|
|
|
templates.Redirect(w, "/admin")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
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)
|
2024-02-26 01:28:40 +00:00
|
|
|
|
2024-03-15 06:08:14 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2024-02-26 01:28:40 +00:00
|
|
|
// Log the change.
|
2024-02-26 01:36:01 +00:00
|
|
|
models.LogEvent(user, currentUser, models.ChangeLogBanned, "users", currentUser.ID, fmt.Sprintf("User ban status updated to: %s", status))
|
2022-08-14 23:27:57 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
case "promote":
|
2023-08-02 03:39:48 +00:00
|
|
|
// Scope check.
|
|
|
|
if !currentUser.HasAdminScope(config.ScopeUserPromote) {
|
|
|
|
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserPromote)
|
|
|
|
templates.Redirect(w, "/admin")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
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)
|
2024-02-26 01:28:40 +00:00
|
|
|
|
|
|
|
// Log the change.
|
2024-02-26 01:36:01 +00:00
|
|
|
models.LogEvent(user, currentUser, models.ChangeLogAdmin, "users", currentUser.ID, fmt.Sprintf("User admin status updated to: %s", action))
|
2022-08-14 23:27:57 +00:00
|
|
|
return
|
|
|
|
}
|
2024-06-15 22:05:50 +00:00
|
|
|
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
|
|
|
|
}
|
2022-08-14 23:27:57 +00:00
|
|
|
case "delete":
|
2023-08-02 03:39:48 +00:00
|
|
|
// Scope check.
|
|
|
|
if !currentUser.HasAdminScope(config.ScopeUserDelete) {
|
|
|
|
session.FlashError(w, r, "Missing admin scope: %s", config.ScopeUserDelete)
|
|
|
|
templates.Redirect(w, "/admin")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-14 23:27:57 +00:00
|
|
|
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")
|
2024-02-26 01:28:40 +00:00
|
|
|
|
2024-03-15 06:08:14 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2024-02-26 01:28:40 +00:00
|
|
|
// Log the change.
|
|
|
|
models.LogDeleted(nil, currentUser, "users", user.ID, fmt.Sprintf("Username %s has been deleted by an admin.", user.Username), nil)
|
2022-08-14 23:27:57 +00:00
|
|
|
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, "/")
|
|
|
|
})
|
|
|
|
}
|