Admin insights block lists page

This commit is contained in:
Noah Petherbridge 2023-12-04 19:57:14 -08:00
parent f618973e80
commit fc8014913d
5 changed files with 157 additions and 6 deletions

View File

@ -32,6 +32,7 @@ const (
// - Impersonate: ability to log in as a user account // - Impersonate: ability to log in as a user account
// - Ban: ability to ban/unban users // - Ban: ability to ban/unban users
// - Delete: ability to delete user accounts // - Delete: ability to delete user accounts
ScopeUserInsight = "admin.user.insights"
ScopeUserImpersonate = "admin.user.impersonate" ScopeUserImpersonate = "admin.user.impersonate"
ScopeUserBan = "admin.user.ban" ScopeUserBan = "admin.user.ban"
ScopeUserPromote = "admin.user.promote" ScopeUserPromote = "admin.user.promote"
@ -62,6 +63,7 @@ func ListAdminScopes() []string {
ScopeCertificationView, ScopeCertificationView,
ScopeForumAdmin, ScopeForumAdmin,
ScopeAdminScopeAdmin, ScopeAdminScopeAdmin,
ScopeUserInsight,
ScopeUserImpersonate, ScopeUserImpersonate,
ScopeUserBan, ScopeUserBan,
ScopeUserDelete, ScopeUserDelete,

View File

@ -46,7 +46,26 @@ func UserActions() http.HandlerFunc {
return return
} }
// Template variables.
var vars = map[string]interface{}{
"Intent": intent,
"User": user,
}
switch intent { 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": case "impersonate":
// Scope check. // Scope check.
if !currentUser.HasAdminScope(config.ScopeUserImpersonate) { if !currentUser.HasAdminScope(config.ScopeUserImpersonate) {
@ -124,10 +143,6 @@ func UserActions() http.HandlerFunc {
return return
} }
var vars = map[string]interface{}{
"Intent": intent,
"User": user,
}
if err := tmpl.Execute(w, r, vars); err != nil { if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -1,6 +1,7 @@
package models package models
import ( import (
"fmt"
"time" "time"
"code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/log"
@ -183,12 +184,93 @@ func BlockedUsernames(user *User) []string {
).Where( ).Where(
"id IN ?", userIDs, "id IN ?", userIDs,
).Scan(&usernames); res.Error != nil { ).Scan(&usernames); res.Error != nil {
log.Error("BlockedUsernames(%d): %s", user.Username, res.Error) log.Error("BlockedUsernames(%s): %s", user.Username, res.Error)
} }
return usernames return usernames
} }
// GetBlocklistInsights returns detailed block lists (both directions) about a user, for admin insight.
func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
// Collect ALL user IDs (both directions) of this user's blocklist.
var (
bs = []*Block{}
forward = []*Block{} // Users they block
reverse = []*Block{} // Users who block the target
userIDs = []uint64{user.ID}
usernames = map[uint64]string{}
)
// Get the complete blocklist and bucket them into forward and reverse.
DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Order("created_at desc").Find(&bs)
for _, row := range bs {
if row.SourceUserID == user.ID {
forward = append(forward, row)
userIDs = append(userIDs, row.TargetUserID)
} else {
reverse = append(reverse, row)
userIDs = append(userIDs, row.SourceUserID)
}
}
// Map all the user IDs to user names.
if len(userIDs) > 0 {
type scanItem struct {
ID uint64
Username string
}
var scan = []scanItem{}
if res := DB.Table(
"users",
).Select(
"id",
"username",
).Where(
"id IN ?", userIDs,
).Scan(&scan); res.Error != nil {
return nil, fmt.Errorf("GetBlocklistInsights(%s): mapping user IDs to names: %s", user.Username, res.Error)
}
for _, row := range scan {
usernames[row.ID] = row.Username
}
}
// Assemble the final result.
var result = &BlocklistInsight{
Blocks: []BlocklistInsightUser{},
BlockedBy: []BlocklistInsightUser{},
}
for _, row := range forward {
if username, ok := usernames[row.TargetUserID]; ok {
result.Blocks = append(result.Blocks, BlocklistInsightUser{
Username: username,
Date: row.CreatedAt,
})
}
}
for _, row := range reverse {
if username, ok := usernames[row.SourceUserID]; ok {
result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{
Username: username,
Date: row.CreatedAt,
})
}
}
return result, nil
}
type BlocklistInsight struct {
Blocks []BlocklistInsightUser
BlockedBy []BlocklistInsightUser
}
type BlocklistInsightUser struct {
Username string
Date time.Time
}
// UnblockUser removes targetUserID from your blocklist. // UnblockUser removes targetUserID from your blocklist.
func UnblockUser(sourceUserID, targetUserID uint64) error { func UnblockUser(sourceUserID, targetUserID uint64) error {
result := DB.Where( result := DB.Where(

View File

@ -459,6 +459,12 @@
<div class="card-content"> <div class="card-content">
<ul class="menu-list"> <ul class="menu-list">
<li>
<a href="/admin/user-action?intent=insights&user_id={{.User.ID}}">
<span class="icon"><i class="fa fa-search"></i></span>
<span>Admin insights <small>(block lists, etc.)</small></span>
</a>
</li>
<li> <li>
<a href="/admin/user-action?intent=impersonate&user_id={{.User.ID}}"> <a href="/admin/user-action?intent=impersonate&user_id={{.User.ID}}">
<span class="icon"><i class="fa fa-ghost"></i></span> <span class="icon"><i class="fa fa-ghost"></i></span>

View File

@ -54,7 +54,53 @@
<input type="hidden" name="intent" value="{{.Intent}}"> <input type="hidden" name="intent" value="{{.Intent}}">
<input type="hidden" name="user_id" value="{{.User.ID}}"> <input type="hidden" name="user_id" value="{{.User.ID}}">
{{if eq .Intent "impersonate"}} {{if eq .Intent "insights"}}
<div class="block content">
<h2>Admin Insights</h2>
<p>
This page gives a peek into the database to glean some insights about a user.
So far, this means taking a look at their block lists: how many people do they
block (and who), and more importantly, how many people are blocking them. It
may be useful information to guage a problematic user, if they are angering or
creeping a lot of people out and ending up on a lot of block lists.
</p>
<h3>Block Lists</h3>
<div class="columns">
<div class="column">
<h5 class="has-text-warning">Forward List <span class="tag is-warning">{{len .BlocklistInsights.Blocks}}</span></h5>
<p>
(Users who {{.User.Username}} is blocking)
</p>
<ul>
{{range .BlocklistInsights.Blocks}}
<li><a href="/u/{{.Username}}">{{.Username}}</a></li>
{{end}}
</ul>
</div>
<div class="column">
<h5 class="has-text-warning">Reverse List <span class="tag is-danger">{{len .BlocklistInsights.BlockedBy}}</span></h5>
<p>
(Users who block {{.User.Username}})
</p>
<ul>
{{range .BlocklistInsights.BlockedBy}}
<li>
<a href="/u/{{.Username}}">{{.Username}}</a>
<small class="has-text-grey" title="{{.Date}}">{{.Date.Format "2006-01-02"}}</small>
</li>
{{end}}
</ul>
</div>
</div>
</div>
{{else if eq .Intent "impersonate"}}
<div class="block content"> <div class="block content">
<h3>With great power...</h3> <h3>With great power...</h3>
<p> <p>