website/pkg/controller/admin/user_actions.go

350 lines
10 KiB
Go
Raw Permalink Normal View History

package admin
import (
"fmt"
"net/http"
"strconv"
"strings"
"code.nonshy.com/nonshy/website/pkg/chat"
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
2023-08-02 03:39:48 +00:00
"code.nonshy.com/nonshy/website/pkg/config"
"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"
)
// 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
}
2023-12-05 03:57:14 +00:00
// Template variables.
var vars = map[string]interface{}{
"Intent": intent,
"User": user,
}
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
}
// 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
// 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
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
}
case "impersonate":
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
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
}
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":
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
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
}
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":
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
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
}
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":
Admin Groups & Permissions Add a permission system for admin users so you can lock down specific admins to a narrower set of features instead of them all having omnipotent powers. * New page: Admin Dashboard -> Admin Permissions Management * Permissions are handled in the form of 'scopes' relevant to each feature or action on the site. Scopes are assigned to Groups, and in turn, admin user accounts are placed in those Groups. * The Superusers group (scope '*') has wildcard permission to all scopes. The permissions dashboard has a create-once action to initialize the Superusers for the first admin who clicks on it, and places that admin in the group. The following are the exhaustive list of permission changes on the site: * Moderator scopes: * Chat room (enter the room with Operator permission) * Forums (can edit or delete user posts on the forum) * Photo Gallery (can see all private/friends-only photos on the site gallery or user profile pages) * Certification photos (with nuanced sub-action permissions) * Approve: has access to the Pending tab to act on incoming pictures * List: can paginate thru past approved/rejected photos * View: can bring up specific user cert photo from their profile * The minimum requirement is Approve or else no cert photo page will load for your admin user. * User Actions (each action individually scoped) * Impersonate * Ban * Delete * Promote to admin * Inner circle whitelist: no longer are admins automatically part of the inner circle unless they have a specialized scope attached. The AdminRequired decorator may also apply scopes on an entire admin route. The following routes have scopes to limit them: * Forum Admin (manage forums and their settings) * Remove from inner circle
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
}
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, "/")
})
}