website/pkg/controller/admin/user_actions.go
Noah Petherbridge 742a5fa1af Auto-Disconnect Users from Chat
Users whose accounts are no longer eligible to be in the chat room will be
disconnected immediately from chat when their account status changes.

The places in nonshy where these disconnects may happen include:

* When the user deactivates or deletes their account.
* When they modify their settings to mark their profile as 'private,' making
  them become a Shy Account.
* When they edit or delete their photos in case they have moved their final
  public photo to be private, making them become a Shy Account.
* When the user deletes their certification photo, or uploads a new cert photo
  to be reviewed (in both cases, losing account certified status).
* When an admin user rejects their certification photo, even retroactively.
* On admin actions against a user, including: banning them, deleting their
  user account.

Other changes made include:

* When signing up an account and e-mail sending is not enabled (e.g. local
  dev environment), the SignupToken is still created and logged to the console
  so you can continue the signup manually.
* On the new account DOB prompt, add a link to manually input their birthdate
  as text similar to on the Age Gate page.
2024-03-15 15:57:05 -07:00

247 lines
7.0 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
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
}
insights, err := models.GetBlocklistInsights(user)
if err != nil {
session.FlashError(w, r, "Error getting blocklist insights: %s", err)
}
vars["BlocklistInsights"] = insights
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 "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, "/")
})
}