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
This commit is contained in:
parent
85d2f4eee9
commit
f4d176a538
|
@ -16,6 +16,7 @@ var (
|
||||||
PageSizeAdminCertification = 20
|
PageSizeAdminCertification = 20
|
||||||
PageSizeAdminFeedback = 20
|
PageSizeAdminFeedback = 20
|
||||||
PageSizeAdminFeedbackNotesPage = 5 // feedback on User Notes page
|
PageSizeAdminFeedbackNotesPage = 5 // feedback on User Notes page
|
||||||
|
PageSizeChangeLog = 20
|
||||||
PageSizeAdminUserNotes = 10 // other users' notes
|
PageSizeAdminUserNotes = 10 // other users' notes
|
||||||
PageSizeSiteGallery = 16
|
PageSizeSiteGallery = 16
|
||||||
PageSizeUserGallery = 16
|
PageSizeUserGallery = 16
|
||||||
|
|
125
pkg/controller/admin/change_log.go
Normal file
125
pkg/controller/admin/change_log.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeLog controller (/admin/changelog)
|
||||||
|
func ChangeLog() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("admin/change_log.html")
|
||||||
|
|
||||||
|
// Whitelist for ordering options.
|
||||||
|
var sortWhitelist = []string{
|
||||||
|
"created_at desc",
|
||||||
|
"created_at asc",
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Query parameters.
|
||||||
|
var (
|
||||||
|
tableName = r.FormValue("table_name")
|
||||||
|
tableID uint64
|
||||||
|
aboutUserID uint64
|
||||||
|
aboutUser = r.FormValue("about_user_id")
|
||||||
|
adminUserID uint64
|
||||||
|
adminUser = r.FormValue("admin_user_id")
|
||||||
|
event = r.FormValue("event")
|
||||||
|
sort = r.FormValue("sort")
|
||||||
|
sortOK bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort options.
|
||||||
|
for _, v := range sortWhitelist {
|
||||||
|
if sort == v {
|
||||||
|
sortOK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sortOK {
|
||||||
|
sort = "created_at desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, err := strconv.Atoi(r.FormValue("table_id")); err == nil {
|
||||||
|
tableID = uint64(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User IDs can be string values to look up by username or email address.
|
||||||
|
if aboutUser != "" {
|
||||||
|
if i, err := strconv.Atoi(aboutUser); err == nil {
|
||||||
|
aboutUserID = uint64(i)
|
||||||
|
} else {
|
||||||
|
if user, err := models.FindUser(aboutUser); err == nil {
|
||||||
|
aboutUserID = user.ID
|
||||||
|
} else {
|
||||||
|
session.FlashError(w, r, "Couldn't find About User ID: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminUser != "" {
|
||||||
|
if i, err := strconv.Atoi(adminUser); err == nil {
|
||||||
|
adminUserID = uint64(i)
|
||||||
|
} else {
|
||||||
|
if user, err := models.FindUser(adminUser); err == nil {
|
||||||
|
adminUserID = user.ID
|
||||||
|
} else {
|
||||||
|
session.FlashError(w, r, "Couldn't find Admin User ID: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pager := &models.Pagination{
|
||||||
|
PerPage: config.PageSizeChangeLog,
|
||||||
|
Sort: sort,
|
||||||
|
}
|
||||||
|
pager.ParsePage(r)
|
||||||
|
|
||||||
|
cl, err := models.PaginateChangeLog(tableName, tableID, aboutUserID, adminUserID, event, pager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Error paginating the change log: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the various user IDs.
|
||||||
|
var (
|
||||||
|
userIDs = []uint64{}
|
||||||
|
)
|
||||||
|
for _, row := range cl {
|
||||||
|
if row.AboutUserID > 0 {
|
||||||
|
userIDs = append(userIDs, row.AboutUserID)
|
||||||
|
}
|
||||||
|
if row.AdminUserID > 0 {
|
||||||
|
userIDs = append(userIDs, row.AdminUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userMap, err := models.MapUsers(nil, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Error mapping user IDs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"ChangeLog": cl,
|
||||||
|
"TableNames": models.ChangeLogTables(),
|
||||||
|
"EventTypes": models.ChangeLogEventTypes,
|
||||||
|
"Pager": pager,
|
||||||
|
"UserMap": userMap,
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
"TableName": tableName,
|
||||||
|
"TableID": tableID,
|
||||||
|
"AboutUserID": aboutUser,
|
||||||
|
"AdminUserID": adminUser,
|
||||||
|
"Event": event,
|
||||||
|
"Sort": sort,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -24,6 +24,14 @@ func MarkPhotoExplicit() http.HandlerFunc {
|
||||||
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 {
|
if idInt, err := strconv.Atoi(r.FormValue("photo_id")); err == nil {
|
||||||
photoID = uint64(idInt)
|
photoID = uint64(idInt)
|
||||||
} else {
|
} else {
|
||||||
|
@ -46,6 +54,12 @@ func MarkPhotoExplicit() http.HandlerFunc {
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Marked photo as Explicit!")
|
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)
|
templates.Redirect(w, next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,9 @@ func BlockUser() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Couldn't unblock this user: %s.", err)
|
session.FlashError(w, r, "Couldn't unblock this user: %s.", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "You have removed %s from your block list.", user.Username)
|
session.Flash(w, r, "You have removed %s from your block list.", user.Username)
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(currentUser, nil, "blocks", user.ID, "Unblocked user "+user.Username+".", nil)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, "/users/blocked")
|
templates.Redirect(w, "/users/blocked")
|
||||||
return
|
return
|
||||||
|
@ -139,6 +142,9 @@ func BlockUser() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Couldn't block this user: %s.", err)
|
session.FlashError(w, r, "Couldn't block this user: %s.", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "You have added %s to your block list.", user.Username)
|
session.Flash(w, r, "You have added %s to your block list.", user.Username)
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "blocks", user.ID, "Blocks user "+user.Username+".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync the block to the BareRTC chat server now, in case either user is currently online.
|
// Sync the block to the BareRTC chat server now, in case either user is currently online.
|
||||||
|
|
|
@ -117,6 +117,9 @@ func PostComment() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Error deleting your commenting: %s", err)
|
session.FlashError(w, r, "Error deleting your commenting: %s", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Your comment has been deleted.")
|
session.Flash(w, r, "Your comment has been deleted.")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, "Deleted a comment.", comment)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, fromURL)
|
templates.Redirect(w, fromURL)
|
||||||
return
|
return
|
||||||
|
@ -151,6 +154,9 @@ func PostComment() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Comment updated!")
|
session.Flash(w, r, "Comment updated!")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, "Updated a comment.\n\n---\n\n"+comment.Message, nil)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, fromURL)
|
templates.Redirect(w, fromURL)
|
||||||
return
|
return
|
||||||
|
@ -168,6 +174,9 @@ func PostComment() http.HandlerFunc {
|
||||||
session.Flash(w, r, "Comment added!")
|
session.Flash(w, r, "Comment added!")
|
||||||
templates.Redirect(w, fromURL)
|
templates.Redirect(w, fromURL)
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "comments", comment.ID, "Posted a new comment.\n\n---\n\n"+message)
|
||||||
|
|
||||||
// Notify the recipient of the comment.
|
// Notify the recipient of the comment.
|
||||||
if notifyUser != nil && notifyUser.ID != currentUser.ID && !notifyUser.NotificationOptOut(config.NotificationOptOutComments) {
|
if notifyUser != nil && notifyUser.ID != currentUser.ID && !notifyUser.NotificationOptOut(config.NotificationOptOutComments) {
|
||||||
notif := &models.Notification{
|
notif := &models.Notification{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package forum
|
package forum
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -74,6 +75,16 @@ func AddEdit() http.HandlerFunc {
|
||||||
|
|
||||||
// Were we editing an existing forum?
|
// Were we editing an existing forum?
|
||||||
if forum != nil {
|
if forum != nil {
|
||||||
|
diffs := []models.FieldDiff{
|
||||||
|
models.NewFieldDiff("Title", forum.Title, title),
|
||||||
|
models.NewFieldDiff("Description", forum.Description, description),
|
||||||
|
models.NewFieldDiff("Category", forum.Category, category),
|
||||||
|
models.NewFieldDiff("Explicit", forum.Explicit, isExplicit),
|
||||||
|
models.NewFieldDiff("Privileged", forum.Privileged, isPrivileged),
|
||||||
|
models.NewFieldDiff("PermitPhotos", forum.PermitPhotos, isPermitPhotos),
|
||||||
|
models.NewFieldDiff("InnerCircle", forum.InnerCircle, isInnerCircle),
|
||||||
|
}
|
||||||
|
|
||||||
forum.Title = title
|
forum.Title = title
|
||||||
forum.Description = description
|
forum.Description = description
|
||||||
forum.Category = category
|
forum.Category = category
|
||||||
|
@ -86,6 +97,9 @@ func AddEdit() http.HandlerFunc {
|
||||||
if err := forum.Save(); err == nil {
|
if err := forum.Save(); err == nil {
|
||||||
session.Flash(w, r, "Forum has been updated!")
|
session.Flash(w, r, "Forum has been updated!")
|
||||||
templates.Redirect(w, "/forum/admin")
|
templates.Redirect(w, "/forum/admin")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(currentUser, nil, "forums", forum.ID, "Updated the forum's settings.", diffs)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
session.FlashError(w, r, "Error saving the forum: %s", err)
|
session.FlashError(w, r, "Error saving the forum: %s", err)
|
||||||
|
@ -119,6 +133,28 @@ func AddEdit() http.HandlerFunc {
|
||||||
if err := models.CreateForum(forum); err == nil {
|
if err := models.CreateForum(forum); err == nil {
|
||||||
session.Flash(w, r, "The forum has been created!")
|
session.Flash(w, r, "The forum has been created!")
|
||||||
templates.Redirect(w, "/forum/admin")
|
templates.Redirect(w, "/forum/admin")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "forums", forum.ID, fmt.Sprintf(
|
||||||
|
"Created a new forum.\n\n"+
|
||||||
|
"* Category: %s\n"+
|
||||||
|
"* Title: %s\n"+
|
||||||
|
"* Fragment: %s\n"+
|
||||||
|
"* Description: %s\n"+
|
||||||
|
"* Explicit: %v\n"+
|
||||||
|
"* Privileged: %v\n"+
|
||||||
|
"* Photos: %v\n"+
|
||||||
|
"* Inner Circle: %v",
|
||||||
|
forum.Category,
|
||||||
|
forum.Title,
|
||||||
|
forum.Fragment,
|
||||||
|
forum.Description,
|
||||||
|
forum.Explicit,
|
||||||
|
forum.Privileged,
|
||||||
|
forum.PermitPhotos,
|
||||||
|
forum.InnerCircle,
|
||||||
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
session.FlashError(w, r, "Error creating the forum: %s", err)
|
session.FlashError(w, r, "Error creating the forum: %s", err)
|
||||||
|
|
|
@ -161,6 +161,11 @@ func NewPost() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Error deleting your post: %s", err)
|
session.FlashError(w, r, "Error deleting your post: %s", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Your post has been deleted.")
|
session.Flash(w, r, "Your post has been deleted.")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, fmt.Sprintf(
|
||||||
|
"Deleted a forum comment on thread %d forum /f/%s", thread.ID, forum.Fragment,
|
||||||
|
), comment)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
||||||
return
|
return
|
||||||
|
@ -315,6 +320,14 @@ func NewPost() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Comment updated!")
|
session.Flash(w, r, "Comment updated!")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(&models.User{ID: comment.UserID}, currentUser, "comments", comment.ID, fmt.Sprintf(
|
||||||
|
"Edited their comment on thread %d (in /f/%s):\n\n%s",
|
||||||
|
thread.ID,
|
||||||
|
forum.Fragment,
|
||||||
|
message,
|
||||||
|
), nil)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
||||||
return
|
return
|
||||||
|
@ -327,6 +340,13 @@ func NewPost() http.HandlerFunc {
|
||||||
} else {
|
} else {
|
||||||
session.Flash(w, r, "Reply added to the thread!")
|
session.Flash(w, r, "Reply added to the thread!")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "comments", reply.ID, fmt.Sprintf(
|
||||||
|
"Commented on thread %d:\n\n%s",
|
||||||
|
thread.ID,
|
||||||
|
message,
|
||||||
|
))
|
||||||
|
|
||||||
// If we're attaching a photo, link it to this reply CommentID.
|
// If we're attaching a photo, link it to this reply CommentID.
|
||||||
if commentPhoto != nil {
|
if commentPhoto != nil {
|
||||||
commentPhoto.CommentID = reply.ID
|
commentPhoto.CommentID = reply.ID
|
||||||
|
@ -428,6 +448,18 @@ func NewPost() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "threads", thread.ID, fmt.Sprintf(
|
||||||
|
"Started a new forum thread on forum /f/%s (%s)\n\n"+
|
||||||
|
"* Has poll? %v\n"+
|
||||||
|
"* Title: %s\n\n%s",
|
||||||
|
forum.Fragment,
|
||||||
|
forum.Title,
|
||||||
|
isPoll,
|
||||||
|
thread.Title,
|
||||||
|
message,
|
||||||
|
))
|
||||||
|
|
||||||
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d", thread.ID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,12 @@ func AddFriend() http.HandlerFunc {
|
||||||
session.Flash(w, r, message)
|
session.Flash(w, r, message)
|
||||||
if verdict == "reject" {
|
if verdict == "reject" {
|
||||||
templates.Redirect(w, "/friends?view=requests")
|
templates.Redirect(w, "/friends?view=requests")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(currentUser, nil, "friends", user.ID, "Rejected friend request from "+user.Username+".", nil)
|
||||||
|
} else {
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(currentUser, nil, "friends", user.ID, "Removed friendship with "+user.Username+".", nil)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, "/friends")
|
templates.Redirect(w, "/friends")
|
||||||
return
|
return
|
||||||
|
@ -85,6 +91,9 @@ func AddFriend() http.HandlerFunc {
|
||||||
session.Flash(w, r, "You have ignored the friend request from %s.", username)
|
session.Flash(w, r, "You have ignored the friend request from %s.", username)
|
||||||
}
|
}
|
||||||
templates.Redirect(w, "/friends")
|
templates.Redirect(w, "/friends")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(currentUser, nil, "friends", user.ID, "Ignored the friend request from "+user.Username+".", nil)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// Post the friend request.
|
// Post the friend request.
|
||||||
|
@ -106,7 +115,13 @@ func AddFriend() http.HandlerFunc {
|
||||||
|
|
||||||
session.Flash(w, r, "You accepted the friend request from %s!", username)
|
session.Flash(w, r, "You accepted the friend request from %s!", username)
|
||||||
templates.Redirect(w, "/friends?view=requests")
|
templates.Redirect(w, "/friends?view=requests")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(currentUser, nil, "friends", user.ID, "Accepted friend request from "+user.Username+".", nil)
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "friends", user.ID, "Sent a friend request to "+user.Username+".")
|
||||||
}
|
}
|
||||||
session.Flash(w, r, "Friend request sent!")
|
session.Flash(w, r, "Friend request sent!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,9 @@ func Certification() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Error saving your User data: %s", err)
|
session.FlashError(w, r, "Error saving your User data: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(currentUser, nil, "certification_photos", currentUser.ID, "Removed their certification photo.", cert)
|
||||||
|
|
||||||
session.Flash(w, r, "Your certification photo has been deleted.")
|
session.Flash(w, r, "Your certification photo has been deleted.")
|
||||||
templates.Redirect(w, r.URL.Path)
|
templates.Redirect(w, r.URL.Path)
|
||||||
return
|
return
|
||||||
|
@ -151,6 +154,9 @@ func Certification() http.HandlerFunc {
|
||||||
log.Error("Certification: failed to notify admins of pending photo: %s", err)
|
log.Error("Certification: failed to notify admins of pending photo: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogCreated(currentUser, "certification_photos", currentUser.ID, "Uploaded a new certification photo.")
|
||||||
|
|
||||||
session.Flash(w, r, "Your certification photo has been uploaded and is now awaiting approval.")
|
session.Flash(w, r, "Your certification photo has been uploaded and is now awaiting approval.")
|
||||||
templates.Redirect(w, r.URL.Path)
|
templates.Redirect(w, r.URL.Path)
|
||||||
return
|
return
|
||||||
|
@ -297,6 +303,9 @@ func AdminCertification() http.HandlerFunc {
|
||||||
user.Certified = false
|
user.Certified = false
|
||||||
user.Save()
|
user.Save()
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogEvent(user, currentUser, models.ChangeLogRejected, "certification_photos", user.ID, "Rejected the certification photo with comment: "+comment)
|
||||||
|
|
||||||
// Did we silently ignore it?
|
// Did we silently ignore it?
|
||||||
if comment == "(ignore)" {
|
if comment == "(ignore)" {
|
||||||
session.FlashError(w, r, "The certification photo was ignored with no comment, and will not notify the sender.")
|
session.FlashError(w, r, "The certification photo was ignored with no comment, and will not notify the sender.")
|
||||||
|
@ -367,6 +376,9 @@ func AdminCertification() http.HandlerFunc {
|
||||||
session.FlashError(w, r, "Note: failed to email user about the approval: %s", err)
|
session.FlashError(w, r, "Note: failed to email user about the approval: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogEvent(user, currentUser, models.ChangeLogApproved, "certification_photos", user.ID, "Approved the certification photo.")
|
||||||
|
|
||||||
session.Flash(w, r, "Certification photo approved!")
|
session.Flash(w, r, "Certification photo approved!")
|
||||||
default:
|
default:
|
||||||
session.FlashError(w, r, "Unsupported verdict option: %s", verdict)
|
session.FlashError(w, r, "Unsupported verdict option: %s", verdict)
|
||||||
|
|
|
@ -42,6 +42,10 @@ func Edit() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case an admin is editing this photo: remember the HTTP request current user,
|
||||||
|
// before the currentUser may be set to the photo's owner below.
|
||||||
|
var requestUser = currentUser
|
||||||
|
|
||||||
// Do we have permission for this photo?
|
// Do we have permission for this photo?
|
||||||
if photo.UserID != currentUser.ID {
|
if photo.UserID != currentUser.ID {
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
|
@ -85,6 +89,14 @@ func Edit() http.HandlerFunc {
|
||||||
isGallery = false
|
isGallery = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diff for the ChangeLog.
|
||||||
|
diffs := []models.FieldDiff{
|
||||||
|
models.NewFieldDiff("Caption", photo.Caption, caption),
|
||||||
|
models.NewFieldDiff("Explicit", photo.Explicit, isExplicit),
|
||||||
|
models.NewFieldDiff("Gallery", photo.Gallery, isGallery),
|
||||||
|
models.NewFieldDiff("Visibility", photo.Visibility, visibility),
|
||||||
|
}
|
||||||
|
|
||||||
photo.Caption = caption
|
photo.Caption = caption
|
||||||
photo.Explicit = isExplicit
|
photo.Explicit = isExplicit
|
||||||
photo.Gallery = isGallery
|
photo.Gallery = isGallery
|
||||||
|
@ -134,6 +146,9 @@ func Edit() http.HandlerFunc {
|
||||||
// Flash success.
|
// Flash success.
|
||||||
session.Flash(w, r, "Photo settings updated!")
|
session.Flash(w, r, "Photo settings updated!")
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogUpdated(currentUser, requestUser, "photos", photo.ID, "Updated the photo's settings.", diffs)
|
||||||
|
|
||||||
// If this picture has moved to Private, revoke any notification we gave about it before.
|
// If this picture has moved to Private, revoke any notification we gave about it before.
|
||||||
if goingPrivate || goingCircle {
|
if goingPrivate || goingCircle {
|
||||||
log.Info("The picture is GOING PRIVATE (to %s), revoke any notifications about it", photo.Visibility)
|
log.Info("The picture is GOING PRIVATE (to %s), revoke any notifications about it", photo.Visibility)
|
||||||
|
@ -190,6 +205,10 @@ func Delete() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case an admin is editing this photo: remember the HTTP request current user,
|
||||||
|
// before the currentUser may be set to the photo's owner below.
|
||||||
|
var requestUser = currentUser
|
||||||
|
|
||||||
// Do we have permission for this photo?
|
// Do we have permission for this photo?
|
||||||
if photo.UserID != currentUser.ID {
|
if photo.UserID != currentUser.ID {
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
|
@ -248,6 +267,9 @@ func Delete() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the change.
|
||||||
|
models.LogDeleted(currentUser, requestUser, "photos", photo.ID, "Deleted the photo.", photo)
|
||||||
|
|
||||||
session.Flash(w, r, "Photo deleted!")
|
session.Flash(w, r, "Photo deleted!")
|
||||||
|
|
||||||
// Return the user to their gallery.
|
// Return the user to their gallery.
|
||||||
|
|
|
@ -2,6 +2,7 @@ package photo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -159,6 +160,19 @@ func Upload() http.HandlerFunc {
|
||||||
user.Save()
|
user.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeLog entry.
|
||||||
|
models.LogCreated(user, "photos", p.ID, fmt.Sprintf(
|
||||||
|
"Uploaded a new photo.\n\n"+
|
||||||
|
"* Caption: %s\n"+
|
||||||
|
"* Visibility: %s\n"+
|
||||||
|
"* Gallery: %v\n"+
|
||||||
|
"* Explicit: %v",
|
||||||
|
p.Caption,
|
||||||
|
p.Visibility,
|
||||||
|
p.Gallery,
|
||||||
|
p.Explicit,
|
||||||
|
))
|
||||||
|
|
||||||
// Notify all of our friends that we posted a new picture.
|
// Notify all of our friends that we posted a new picture.
|
||||||
go notifyFriendsNewPhoto(p, user)
|
go notifyFriendsNewPhoto(p, user)
|
||||||
|
|
||||||
|
|
234
pkg/models/change_log.go
Normal file
234
pkg/models/change_log.go
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeLog table to track updates to the database.
|
||||||
|
type ChangeLog struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
AboutUserID uint64 `gorm:"index"`
|
||||||
|
AdminUserID uint64 `gorm:"index"` // if an admin edits a user's item
|
||||||
|
TableName string `gorm:"index"`
|
||||||
|
TableID uint64 `gorm:"index"`
|
||||||
|
Event string `gorm:"index"`
|
||||||
|
Message string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types of ChangeLog events.
|
||||||
|
const (
|
||||||
|
ChangeLogCreated = "created"
|
||||||
|
ChangeLogUpdated = "updated"
|
||||||
|
ChangeLogDeleted = "deleted"
|
||||||
|
|
||||||
|
// Certification photos.
|
||||||
|
ChangeLogApproved = "approved"
|
||||||
|
ChangeLogRejected = "rejected"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChangeLogEventTypes = []string{
|
||||||
|
ChangeLogCreated,
|
||||||
|
ChangeLogUpdated,
|
||||||
|
ChangeLogDeleted,
|
||||||
|
ChangeLogApproved,
|
||||||
|
ChangeLogRejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateChangeLog lists the change logs.
|
||||||
|
func PaginateChangeLog(tableName string, tableID, aboutUserID, adminUserID uint64, event string, pager *Pagination) ([]*ChangeLog, error) {
|
||||||
|
var (
|
||||||
|
cl = []*ChangeLog{}
|
||||||
|
where = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if tableName != "" {
|
||||||
|
where = append(where, "table_name = ?")
|
||||||
|
placeholders = append(placeholders, tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tableID != 0 {
|
||||||
|
where = append(where, "table_id = ?")
|
||||||
|
placeholders = append(placeholders, tableID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUserID != 0 {
|
||||||
|
where = append(where, "about_user_id = ?")
|
||||||
|
placeholders = append(placeholders, aboutUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminUserID != 0 {
|
||||||
|
where = append(where, "admin_user_id = ?")
|
||||||
|
placeholders = append(placeholders, adminUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event != "" {
|
||||||
|
where = append(where, "event = ?")
|
||||||
|
placeholders = append(placeholders, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := DB.Model(&ChangeLog{}).Where(
|
||||||
|
strings.Join(where, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Order(
|
||||||
|
pager.Sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
query.Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cl)
|
||||||
|
return cl, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeLogTables returns all the distinct table_names appearing in the change log.
|
||||||
|
func ChangeLogTables() []string {
|
||||||
|
var result = []string{}
|
||||||
|
|
||||||
|
query := DB.Model(&ChangeLog{}).
|
||||||
|
Select("DISTINCT change_logs.table_name").
|
||||||
|
Group("change_logs.table_name").
|
||||||
|
Find(&result)
|
||||||
|
if query.Error != nil {
|
||||||
|
log.Error("ChangeLogTables: %s", query.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogEvent puts in a generic/miscellaneous change log event (e.g. certification photo updates).
|
||||||
|
func LogEvent(aboutUser, adminUser *User, event, tableName string, tableID uint64, message string) (*ChangeLog, error) {
|
||||||
|
cl := &ChangeLog{
|
||||||
|
TableName: tableName,
|
||||||
|
TableID: tableID,
|
||||||
|
Event: event,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
cl.AboutUserID = aboutUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminUser != nil && adminUser != aboutUser {
|
||||||
|
cl.AdminUserID = adminUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
result := DB.Create(cl)
|
||||||
|
return cl, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogCreated puts in a ChangeLog "created" event.
|
||||||
|
func LogCreated(aboutUser *User, tableName string, tableID uint64, message string) (*ChangeLog, error) {
|
||||||
|
cl := &ChangeLog{
|
||||||
|
TableName: tableName,
|
||||||
|
TableID: tableID,
|
||||||
|
Event: ChangeLogCreated,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
cl.AboutUserID = aboutUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
result := DB.Create(cl)
|
||||||
|
return cl, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDeleted puts in a ChangeLog "deleted" event.
|
||||||
|
func LogDeleted(aboutUser, adminUser *User, tableName string, tableID uint64, message string, original interface{}) (*ChangeLog, error) {
|
||||||
|
// If the original model is given, JSON serialize it nicely.
|
||||||
|
if original != nil {
|
||||||
|
w := bytes.NewBuffer([]byte{})
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("\n", "* ")
|
||||||
|
if err := enc.Encode(original); err != nil {
|
||||||
|
log.Error("LogDeleted(%s %d): couldn't encode original model to JSON: %s", tableName, tableID, err)
|
||||||
|
} else {
|
||||||
|
message += "\n\n" + w.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := &ChangeLog{
|
||||||
|
TableName: tableName,
|
||||||
|
TableID: tableID,
|
||||||
|
Event: ChangeLogDeleted,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
cl.AboutUserID = aboutUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminUser != nil && adminUser != aboutUser {
|
||||||
|
cl.AdminUserID = adminUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
result := DB.Create(cl)
|
||||||
|
return cl, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldDiff struct {
|
||||||
|
Key string
|
||||||
|
Before interface{}
|
||||||
|
After interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldDiff(key string, before, after interface{}) FieldDiff {
|
||||||
|
return FieldDiff{
|
||||||
|
Key: key,
|
||||||
|
Before: before,
|
||||||
|
After: after,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogUpdated puts in a ChangeLog "updated" event.
|
||||||
|
func LogUpdated(aboutUser, adminUser *User, tableName string, tableID uint64, message string, diffs []FieldDiff) (*ChangeLog, error) {
|
||||||
|
// Append field diffs to the message?
|
||||||
|
lines := []string{message}
|
||||||
|
if len(diffs) > 0 {
|
||||||
|
lines = append(lines, "")
|
||||||
|
for _, row := range diffs {
|
||||||
|
var (
|
||||||
|
before = fmt.Sprintf("%v", row.Before)
|
||||||
|
after = fmt.Sprintf("%v", row.After)
|
||||||
|
)
|
||||||
|
|
||||||
|
if before != after {
|
||||||
|
lines = append(lines,
|
||||||
|
fmt.Sprintf("* **%s** changed to <code>%s</code> from <code>%s</code>",
|
||||||
|
row.Key,
|
||||||
|
strings.ReplaceAll(after, "`", "'"),
|
||||||
|
strings.ReplaceAll(before, "`", "'"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := &ChangeLog{
|
||||||
|
TableName: tableName,
|
||||||
|
TableID: tableID,
|
||||||
|
Event: ChangeLogUpdated,
|
||||||
|
Message: strings.Join(lines, "\n"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if aboutUser != nil {
|
||||||
|
cl.AboutUserID = aboutUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminUser != nil && adminUser != aboutUser {
|
||||||
|
cl.AdminUserID = adminUser.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
result := DB.Create(cl)
|
||||||
|
return cl, result.Error
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type Comment struct {
|
||||||
TableName string `gorm:"index"`
|
TableName string `gorm:"index"`
|
||||||
TableID uint64 `gorm:"index"`
|
TableID uint64 `gorm:"index"`
|
||||||
UserID uint64 `gorm:"index"`
|
UserID uint64 `gorm:"index"`
|
||||||
User User
|
User User `json:"-"`
|
||||||
Message string
|
Message string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
|
@ -31,4 +31,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&UserLocation{})
|
DB.AutoMigrate(&UserLocation{})
|
||||||
DB.AutoMigrate(&UserNote{})
|
DB.AutoMigrate(&UserNote{})
|
||||||
DB.AutoMigrate(&TwoFactor{})
|
DB.AutoMigrate(&TwoFactor{})
|
||||||
|
DB.AutoMigrate(&ChangeLog{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,13 @@ import (
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `gorm:"primaryKey"`
|
ID uint64 `gorm:"primaryKey"`
|
||||||
Username string `gorm:"uniqueIndex"`
|
Username string `gorm:"uniqueIndex"`
|
||||||
Email string `gorm:"uniqueIndex"`
|
Email string `gorm:"uniqueIndex" json:"-"`
|
||||||
HashedPassword string
|
HashedPassword string `json:"-"`
|
||||||
IsAdmin bool `gorm:"index"`
|
IsAdmin bool `gorm:"index"`
|
||||||
Status UserStatus `gorm:"index"` // active, disabled
|
Status UserStatus `gorm:"index"` // active, disabled
|
||||||
Visibility UserVisibility `gorm:"index"` // public, private
|
Visibility UserVisibility `gorm:"index"` // public, private
|
||||||
Name *string
|
Name *string
|
||||||
Birthdate time.Time
|
Birthdate time.Time `json:"-"`
|
||||||
Certified bool
|
Certified bool
|
||||||
Explicit bool `gorm:"index"` // user has opted-in to see explicit content
|
Explicit bool `gorm:"index"` // user has opted-in to see explicit content
|
||||||
InnerCircle bool `gorm:"index"` // user is in the inner circle
|
InnerCircle bool `gorm:"index"` // user is in the inner circle
|
||||||
|
@ -34,10 +34,10 @@ type User struct {
|
||||||
LastLoginAt time.Time `gorm:"index"`
|
LastLoginAt time.Time `gorm:"index"`
|
||||||
|
|
||||||
// Relational tables.
|
// Relational tables.
|
||||||
ProfileField []ProfileField
|
ProfileField []ProfileField `json:"-"`
|
||||||
ProfilePhotoID *uint64
|
ProfilePhotoID *uint64
|
||||||
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
||||||
AdminGroups []*AdminGroup `gorm:"many2many:admin_group_users;"`
|
AdminGroups []*AdminGroup `gorm:"many2many:admin_group_users;" json:"-"`
|
||||||
|
|
||||||
// Current user's relationship to this user -- not stored in DB.
|
// Current user's relationship to this user -- not stored in DB.
|
||||||
UserRelationship UserRelationship `gorm:"-"`
|
UserRelationship UserRelationship `gorm:"-"`
|
||||||
|
|
|
@ -101,6 +101,7 @@ func New() http.Handler {
|
||||||
mux.Handle("/forum/admin/edit", middleware.AdminRequired(config.ScopeForumAdmin, forum.AddEdit()))
|
mux.Handle("/forum/admin/edit", middleware.AdminRequired(config.ScopeForumAdmin, forum.AddEdit()))
|
||||||
mux.Handle("/inner-circle/remove", middleware.LoginRequired(account.RemoveCircle()))
|
mux.Handle("/inner-circle/remove", middleware.LoginRequired(account.RemoveCircle()))
|
||||||
mux.Handle("/admin/photo/mark-explicit", middleware.AdminRequired(config.ScopePhotoModerator, admin.MarkPhotoExplicit()))
|
mux.Handle("/admin/photo/mark-explicit", middleware.AdminRequired(config.ScopePhotoModerator, admin.MarkPhotoExplicit()))
|
||||||
|
mux.Handle("GET /admin/changelog", middleware.AdminRequired("", admin.ChangeLog()))
|
||||||
|
|
||||||
// JSON API endpoints.
|
// JSON API endpoints.
|
||||||
mux.HandleFunc("GET /v1/version", api.Version())
|
mux.HandleFunc("GET /v1/version", api.Version())
|
||||||
|
|
212
web/templates/admin/change_log.html
Normal file
212
web/templates/admin/change_log.html
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
{{define "title"}}Change Log{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
{{$Root := .}}
|
||||||
|
<section class="hero is-danger is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Change Logs
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form action="{{.Request.URL.Path}}" method="GET">
|
||||||
|
<div class="p-4">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
Found {{FormatNumberCommas .Pager.Total}} user{{Pluralize64 .Pager.Total}}
|
||||||
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
|
||||||
|
<div class="card nonshy-collapsible-mobile">
|
||||||
|
<header class="card-header has-background-link-light">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Search Filters
|
||||||
|
</p>
|
||||||
|
<button class="card-header-icon" type="button">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-angle-up"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="columns">
|
||||||
|
|
||||||
|
<div class="column pr-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Table Name:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="table_name" name="table_name">
|
||||||
|
<option value="">(Any)</option>
|
||||||
|
{{range .TableNames}}
|
||||||
|
<option value="{{.}}"{{if eq $Root.TableName .}} selected{{end}}>{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column px-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Table ID:</label>
|
||||||
|
<input type="number" class="input"
|
||||||
|
name="table_id"
|
||||||
|
autocomplete="off"
|
||||||
|
value="{{.TableID}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column px-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">About User:</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="about_user_id"
|
||||||
|
autocomplete="off"
|
||||||
|
value="{{.AboutUserID}}">
|
||||||
|
<p class="help">
|
||||||
|
ID number <em>or</em> username or email address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column px-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Admin User:</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="admin_user_id"
|
||||||
|
autocomplete="off"
|
||||||
|
value="{{.AdminUserID}}">
|
||||||
|
<p class="help">
|
||||||
|
ID number <em>or</em> username or email address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column px-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Event Type:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="event" name="event">
|
||||||
|
<option value="">(Any)</option>
|
||||||
|
{{range .EventTypes}}
|
||||||
|
<option value="{{.}}"{{if eq $Root.Event .}} selected{{end}}>{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column px-1">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Sort:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="sort" name="sort">
|
||||||
|
<option value="created_at desc"{{if eq $Root.Sort "created_at desc"}} selected{{end}}>Newest first</option>
|
||||||
|
<option value="created_at asc"{{if eq $Root.Sort "created_at asc"}} selected{{end}}>Oldest first</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
<span>Search</span>
|
||||||
|
<span class="icon"><i class="fa fa-search"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{SimplePager .Pager}}
|
||||||
|
|
||||||
|
<table class="table is-fullwidth is-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>About User</th>
|
||||||
|
<th>Admin User</th>
|
||||||
|
<th>Table</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="is-size-7">
|
||||||
|
{{range .ChangeLog}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?{{QueryPlus "event" .Event}}">{{.Event}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $Root.UserMap.Has .AboutUserID}}
|
||||||
|
{{$User := $Root.UserMap.Get .AboutUserID}}
|
||||||
|
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
|
||||||
|
<small>(ID: {{$User.ID}})</small>
|
||||||
|
|
||||||
|
<!-- Filter by this user -->
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?{{QueryPlus "about_user_id" .AboutUserID}}">
|
||||||
|
<i class="fa fa-search" title="Filter by this user ID"></i>
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
{{.AboutUserID}}
|
||||||
|
<i class="fa fa-exclamation-triangle has-text-danger" title="User Not Found" onclick="alert('User Not Found')"></i>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $Root.UserMap.Has .AdminUserID}}
|
||||||
|
{{$User := $Root.UserMap.Get .AdminUserID}}
|
||||||
|
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
|
||||||
|
<small>(ID: {{$User.ID}})</small>
|
||||||
|
|
||||||
|
<!-- Filter by this user -->
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?{{QueryPlus "admin_user_id" .AdminUserID}}">
|
||||||
|
<i class="fa fa-search" title="Filter by this user ID"></i>
|
||||||
|
</a>
|
||||||
|
{{else if .AdminUserID}}
|
||||||
|
{{.AdminUserID}}
|
||||||
|
<i class="fa fa-exclamation-triangle has-text-danger" title="User Not Found" onclick="alert('User Not Found')"></i>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?{{QueryPlus "table_name" .TableName}}">{{.TableName}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?{{QueryPlus "table_name" .TableName "table_id" .TableID}}">{{.TableID}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="content">
|
||||||
|
{{ToMarkdown .Message}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{.CreatedAt.Format "2006-01-02 15:04:05"}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{SimplePager .Pager}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Style override, allow a horizontal scrollbar if needed, e.g. mobile -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
|
@ -142,6 +142,12 @@
|
||||||
Gallery: Admin View
|
Gallery: Admin View
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/changelog">
|
||||||
|
<i class="fa fa-clipboard-list mr-2"></i>
|
||||||
|
Change Log Viewer
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/scopes">
|
<a href="/admin/scopes">
|
||||||
<i class="fa fa-gavel mr-2"></i>
|
<i class="fa fa-gavel mr-2"></i>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user