Skip to content

Commit 6882177

Browse files
committed
BLENDER: Add Admin page for Users with links in their profile
This can help to spot early creations of spam accounts.
1 parent c58ce4d commit 6882177

File tree

5 files changed

+148
-0
lines changed

5 files changed

+148
-0
lines changed

options/locale/locale_en-US.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,8 @@ spamreport.modal_info = Report a user as a spammer to site admins.
722722
spamreport.modal_action = Report spam
723723
spamreport.profile_button = Report spam
724724
725+
726+
725727
[settings]
726728
profile = Profile
727729
account = Account
@@ -2956,6 +2958,7 @@ last_page = Last
29562958
total = Total: %d
29572959
settings = Admin Settings
29582960
spamreports = Spam Reports
2961+
users_with_links = Users (Potential Spam)
29592962
29602963
dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="%s">the blog</a> for more details.
29612964
dashboard.statistic = Summary
@@ -3103,6 +3106,11 @@ users.list_status_filter.is_2fa_enabled = 2FA Enabled
31033106
users.list_status_filter.not_2fa_enabled = 2FA Disabled
31043107
users.details = User Details
31053108
3109+
users.description = Description
3110+
users.location = Location
3111+
users.website = Website
3112+
users.report_spam = Report Users for Spam
3113+
31063114
emails.email_manage_panel = User Email Management
31073115
emails.primary = Primary
31083116
emails.activated = Activated

routers/web/admin/users_with_links.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package admin
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
user_model "code.gitea.io/gitea/models/user"
8+
"code.gitea.io/gitea/modules/optional"
9+
"code.gitea.io/gitea/modules/setting"
10+
"code.gitea.io/gitea/modules/templates"
11+
"code.gitea.io/gitea/models/db"
12+
"code.gitea.io/gitea/services/context"
13+
)
14+
15+
const tplUsersWithLinks templates.TplName = "admin/users_with_links"
16+
17+
// UsersWithLinks renders a list of users that contain hyperlinks in bio fields
18+
func UsersWithLinks(ctx *context.Context) {
19+
ctx.Data["Title"] = ctx.Tr("admin.users.with_links")
20+
ctx.Data["PageIsAdminUsers"] = true
21+
22+
// Parse filters from query parameters
23+
statusActive := ctx.FormString("status_filter[is_active]")
24+
statusAdmin := ctx.FormString("status_filter[is_admin]")
25+
statusRestricted := ctx.FormString("status_filter[is_restricted]")
26+
status2fa := ctx.FormString("status_filter[is_2fa_enabled]")
27+
statusProhibit := ctx.FormString("status_filter[is_prohibit_login]")
28+
29+
sort := ctx.FormString("sort")
30+
if sort == "" {
31+
sort = "created_unix"
32+
}
33+
ctx.Data["SortType"] = sort
34+
35+
// Build search options
36+
opts := &user_model.SearchUserOptions{
37+
ListOptions: db.ListOptions{
38+
Page: ctx.FormInt("page"),
39+
PageSize: setting.UI.Admin.UserPagingNum,
40+
},
41+
OrderBy: db.SearchOrderBy(sort),
42+
Type: user_model.UserTypeIndividual,
43+
44+
IsActive: optional.ParseBool(statusActive),
45+
IsAdmin: optional.ParseBool(statusAdmin),
46+
IsRestricted: optional.ParseBool(statusRestricted),
47+
IsTwoFactorEnabled: optional.ParseBool(status2fa),
48+
IsProhibitLogin: optional.ParseBool(statusProhibit),
49+
50+
IncludeReserved: true,
51+
SearchByEmail: true,
52+
}
53+
54+
users, count, err := user_model.SearchUsers(ctx, opts)
55+
if err != nil {
56+
ctx.ServerError("SearchUsers", err)
57+
return
58+
}
59+
60+
// Filter users with hyperlinks in bio fields
61+
filtered := make([]*user_model.User, 0, len(users))
62+
for _, u := range users {
63+
if containsHyperlink(u.FullName) || containsHyperlink(u.Description) ||
64+
containsHyperlink(u.Location) || containsHyperlink(u.Website) {
65+
filtered = append(filtered, u)
66+
}
67+
}
68+
69+
ctx.Data["Users"] = filtered
70+
ctx.Data["Total"] = len(filtered)
71+
ctx.Data["CanDeleteUsers"] = true
72+
73+
// Pagination
74+
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
75+
pager.AddParamFromRequest(ctx.Req)
76+
ctx.Data["Page"] = pager
77+
78+
ctx.HTML(http.StatusOK, tplUsersWithLinks)
79+
}
80+
81+
func containsHyperlink(text string) bool {
82+
text = strings.ToLower(text)
83+
return strings.Contains(text, "http://") || strings.Contains(text, "https://")
84+
}

routers/web/web.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,9 @@ func registerWebRoutes(m *web.Router) {
761761
})
762762

763763
// BLENDER: spam reporting
764+
m.Group("/users_with_links", func() {
765+
m.Get("", admin.UsersWithLinks)
766+
})
764767
m.Group("/spamreports", func() {
765768
m.Get("", admin.SpamReports)
766769
m.Post("", admin.SpamReportsPost)

templates/admin/navbar.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<a class="{{if .PageIsSpamReports}}active {{end}}item" href="{{AppSubUrl}}/-/admin/spamreports">
3232
{{ctx.Locale.Tr "admin.spamreports"}}
3333
</a>
34+
<a class="{{if .PageIsSpamReports}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users_with_links">
35+
{{ctx.Locale.Tr "admin.users_with_links"}}
36+
</a>
3437
</div>
3538
</details>
3639
<details class="item toggleable-item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>

templates/admin/users_with_links.tmpl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
2+
<div class="admin-setting-content">
3+
<h4 class="ui top attached header">
4+
{{ctx.Locale.Tr "admin.users_with_links"}} ({{.Total}})
5+
</h4>
6+
7+
<div class="ui attached table segment">
8+
<table class="ui very basic striped table unstackable">
9+
<thead>
10+
<tr>
11+
<th><input type="checkbox" id="select-all"></th>
12+
<th>ID</th>
13+
<th>{{ctx.Locale.Tr "admin.users.name"}}</th>
14+
<th>{{ctx.Locale.Tr "admin.users.full_name"}}</th>
15+
<th>{{ctx.Locale.Tr "admin.users.description"}}</th>
16+
<th>{{ctx.Locale.Tr "admin.users.location"}}</th>
17+
<th>{{ctx.Locale.Tr "admin.users.website"}}</th>
18+
<th>{{ctx.Locale.Tr "admin.users.created"}}</th>
19+
</tr>
20+
</thead>
21+
<tbody>
22+
{{range .Users}}
23+
<tr>
24+
<td><input type="checkbox" name="user_ids" value="{{.ID}}"></td>
25+
<td>{{.ID}}</td>
26+
<td><a href="{{.HomeLink}}">{{.Name}}</a></td>
27+
<td>{{.FullName}}</td>
28+
<td class="gt-ellipsis tw-max-w-48">{{.Description}}</td>
29+
<td class="gt-ellipsis tw-max-w-48">{{.Location}}</td>
30+
<td class="gt-ellipsis tw-max-w-48"><a href="{{.Website}}" target="_blank">{{.Website}}</a></td>
31+
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
32+
</tr>
33+
{{else}}
34+
<tr class="no-results-row">
35+
<td class="tw-text-center" colspan="8">{{ctx.Locale.Tr "no_results_found"}}</td>
36+
</tr>
37+
{{end}}
38+
</tbody>
39+
</table>
40+
</div>
41+
42+
{{template "base/paginate" .}}
43+
</div>
44+
<script>
45+
document.getElementById('select-all').addEventListener('click', function () {
46+
const checkboxes = document.querySelectorAll('input[name="user_ids"]');
47+
for (const cb of checkboxes) cb.checked = this.checked;
48+
});
49+
</script>
50+
{{template "admin/layout_footer" .}}

0 commit comments

Comments
 (0)