Admin insights block lists page
This commit is contained in:
parent
f618973e80
commit
fc8014913d
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user