website/pkg/controller/account/user_note.go
Noah Petherbridge 20d04fc370 Admin Transparency Page
* Add a transparency page where regular user accounts can list the roles and
  permissions that an admin user has access to. It is available by clicking on
  the "Admin" badge on that user's profile page.
* Add additional admin scopes to lock down more functionality:
  * User feedback and reports
  * Change logs
  * User notes and admin notes
* Add friendly descriptions to what all the scopes mean in practice.
* Don't show admin notification badges to admins who aren't allowed to act on
  those notifications.
* Update the admin dashboard page and documentation for admins.
2024-05-09 15:50:46 -07:00

303 lines
7.7 KiB
Go

package account
import (
"net/http"
"net/url"
"strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/middleware"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// User notes page (/notes/u/username)
func UserNotes() http.HandlerFunc {
tmpl := templates.Must("account/user_notes.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Parse the username out of the URL parameters.
var username = r.PathValue("username")
// Find this user.
user, err := models.FindUser(username)
if err != nil {
templates.NotFoundPage(w, r)
return
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String()))
return
}
// Is the site under a Maintenance Mode restriction?
if middleware.MaintenanceMode(currentUser, w, r) {
return
}
// Banned or disabled? Only admin can view then.
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
templates.NotFoundPage(w, r)
return
}
// Is either one blocking?
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
templates.NotFoundPage(w, r)
return
}
// Look up our current note about this person.
var myNote = models.GetNoteBetweenUsers(currentUser, user)
// Are we submitting a note?
if r.Method == http.MethodPost {
var message = r.FormValue("message")
// Update & save our note.
if message == "" {
// Delete it.
if err := myNote.Delete(); err != nil && err != models.ErrNoUserNoteToDelete {
session.FlashError(w, r, "Error deleting your note: %s", err)
} else if err == nil {
session.Flash(w, r, "Your note was deleted!")
}
} else {
// Update it.
myNote.Message = message
if err := myNote.Save(); err != nil {
session.FlashError(w, r, "Error saving your note: %s", err)
} else {
session.Flash(w, r, "Your notes have been saved!")
}
}
templates.Redirect(w, r.URL.Path)
return
}
// Admin view: paginate their feedback & reports.
var (
feedback = []*models.Feedback{}
otherNotes = []*models.UserNote{}
userMap = models.UserMap{}
notePager = &models.Pagination{
Page: 1,
PerPage: config.PageSizeAdminUserNotes,
Sort: "updated_at desc",
}
fbPager = &models.Pagination{
Page: 1,
PerPage: config.PageSizeAdminFeedbackNotesPage,
Sort: "created_at desc",
}
)
notePager.ParsePage(r)
fbPager.ParsePage(r)
if currentUser.IsAdmin {
// Paginate all notes written about this user.
if on, err := models.PaginateUserNotes(user, notePager); err != nil {
session.FlashError(w, r, "Paginating user notes: %s", err)
} else {
otherNotes = on
}
// Paginate feedback & reports.
if fb, err := models.PaginateFeedbackAboutUser(user, fbPager); err != nil {
session.FlashError(w, r, "Paginating feedback on this user: %s", err)
} else {
feedback = fb
}
// Map user IDs for the Feedback Reply-To line and the Note Givers for the paginated notes.
var userIDs = []uint64{}
for _, p := range feedback {
if p.UserID > 0 {
userIDs = append(userIDs, p.UserID)
}
}
for _, p := range otherNotes {
if p.UserID > 0 {
userIDs = append(userIDs, p.UserID)
}
}
if um, err := models.MapUsers(currentUser, userIDs); err != nil {
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
} else {
userMap = um
}
}
vars := map[string]interface{}{
"User": user,
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
"NoteCount": models.CountNotesAboutUser(currentUser, user),
"FriendCount": models.CountFriends(user.ID),
"MyNote": myNote,
// Admin concerns.
"Feedback": feedback,
"FeedbackPager": fbPager,
"OtherNotes": otherNotes,
"NotePager": notePager,
"UserMap": userMap,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// My user notes page (/notes/me)
func MyNotes() http.HandlerFunc {
tmpl := templates.Must("account/my_user_notes.html")
// Whitelist for ordering options.
var sortWhitelist = []string{
"updated_at desc",
"updated_at asc",
"username desc",
"username asc",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Filter parameters.
var (
search = r.FormValue("search")
sort = r.FormValue("sort")
adminNotes = r.FormValue("admin_notes") == "true"
sortOK bool
)
// Sort options.
for _, v := range sortWhitelist {
if sort == v {
sortOK = true
break
}
}
if !sortOK {
sort = sortWhitelist[0]
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String()))
return
}
// Is the site under a Maintenance Mode restriction?
if middleware.MaintenanceMode(currentUser, w, r) {
return
}
// Admin notes?
if adminNotes && !currentUser.HasAdminScope(config.ScopeUserNotes) {
adminNotes = false
}
// Are we deleting a note?
if r.Method == http.MethodPost {
var (
intent = r.PostFormValue("intent")
idStr = r.PostFormValue("id")
)
noteID, err := strconv.Atoi(idStr)
if err != nil {
session.FlashError(w, r, "Invalid note ID.")
templates.Redirect(w, r.URL.Path)
return
}
note, err := models.GetNote(uint64(noteID))
if err != nil {
session.FlashError(w, r, "Couldn't find that note.")
templates.Redirect(w, r.URL.Path)
return
}
// Assert it is our note to edit.
if note.UserID != currentUser.ID {
session.FlashError(w, r, "That is not your note to edit.")
templates.Redirect(w, r.URL.Path)
return
}
if intent == "delete" {
// Delete it!
if err := note.Delete(); err != nil {
session.FlashError(w, r, "Error deleting the note: %s.", err)
templates.Redirect(w, r.URL.Path)
return
}
session.Flash(w, r, "That note has been deleted!")
}
templates.Redirect(w, r.URL.Path)
return
}
var (
pager = &models.Pagination{
Page: 1,
PerPage: config.PageSizeAdminUserNotes,
Sort: sort,
}
userIDs = []uint64{}
notes []*models.UserNote
adminUserMap models.UserMap
)
pager.ParsePage(r)
if adminNotes {
notes, err = models.PaginateAdminUserNotes(search, pager)
if userMap, err := models.MapAdminUsers(currentUser); err == nil {
adminUserMap = userMap
}
} else {
notes, err = models.PaginateMyUserNotes(currentUser, search, pager)
}
if err != nil {
session.FlashError(w, r, "Error getting your user notes: %s", err)
templates.Redirect(w, "/")
return
}
// Map user IDs to users.
for _, note := range notes {
userIDs = append(userIDs, note.AboutUserID)
}
userMap, err := models.MapUsers(currentUser, userIDs)
if err != nil {
log.Error("MyUserNotes: couldn't MapUsers: %s", err)
}
vars := map[string]interface{}{
"Notes": notes,
"Pager": pager,
"UserMap": userMap, // users talked about on this page
"AdminUserMap": adminUserMap, // other admin note authors for AdminNotes view
// Search filters
"Search": search,
"Sort": sort,
"AdminNotes": adminNotes,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}