diff --git a/pkg/config/admin_scopes.go b/pkg/config/admin_scopes.go index d71d430..21a0e4c 100644 --- a/pkg/config/admin_scopes.go +++ b/pkg/config/admin_scopes.go @@ -32,6 +32,7 @@ const ( // - Impersonate: ability to log in as a user account // - Ban: ability to ban/unban users // - Delete: ability to delete user accounts + ScopeUserInsight = "admin.user.insights" ScopeUserImpersonate = "admin.user.impersonate" ScopeUserBan = "admin.user.ban" ScopeUserPromote = "admin.user.promote" @@ -62,6 +63,7 @@ func ListAdminScopes() []string { ScopeCertificationView, ScopeForumAdmin, ScopeAdminScopeAdmin, + ScopeUserInsight, ScopeUserImpersonate, ScopeUserBan, ScopeUserDelete, diff --git a/pkg/controller/admin/user_actions.go b/pkg/controller/admin/user_actions.go index 3b2b1bf..1046601 100644 --- a/pkg/controller/admin/user_actions.go +++ b/pkg/controller/admin/user_actions.go @@ -46,7 +46,26 @@ func UserActions() http.HandlerFunc { 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) { @@ -124,10 +143,6 @@ func UserActions() http.HandlerFunc { return } - var vars = map[string]interface{}{ - "Intent": intent, - "User": user, - } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pkg/models/blocklist.go b/pkg/models/blocklist.go index f3e169b..1e61b89 100644 --- a/pkg/models/blocklist.go +++ b/pkg/models/blocklist.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "code.nonshy.com/nonshy/website/pkg/log" @@ -183,12 +184,93 @@ func BlockedUsernames(user *User) []string { ).Where( "id IN ?", userIDs, ).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 } +// 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. func UnblockUser(sourceUserID, targetUserID uint64) error { result := DB.Where( diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index a7ae685..9b5ce90 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -459,6 +459,12 @@