-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Display User signin metadata in admin dashboard #33955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
5d716fb
996fae0
7888a5a
ffb849d
abebe11
ec44bdb
2fcb0cb
b1b8d9b
f26cb7c
bf9fc6c
2a49d34
03af3b8
dceac5d
f45ff5a
2087b53
57771b2
84f12a2
7a44d4a
6476a7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright 2025 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package admin | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
user_model "code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/setting" | ||
"code.gitea.io/gitea/modules/templates" | ||
"code.gitea.io/gitea/services/context" | ||
) | ||
|
||
const ( | ||
tplIPs templates.TplName = "admin/ips/list" | ||
) | ||
|
||
// trimPortFromIP removes the client port from an IP address | ||
// Handles both IPv4 and IPv6 addresses with ports | ||
func trimPortFromIP(ip string) string { | ||
// Handle IPv6 with brackets: [IPv6]:port | ||
if strings.HasPrefix(ip, "[") { | ||
// If there's no port, return as is | ||
if !strings.Contains(ip, "]:") { | ||
return ip | ||
} | ||
// Remove the port part after ]: | ||
return strings.Split(ip, "]:")[0] + "]" | ||
} | ||
|
||
// Count colons to differentiate between IPv4 and IPv6 | ||
colonCount := strings.Count(ip, ":") | ||
|
||
// Handle IPv4 with port (single colon) | ||
if colonCount == 1 { | ||
return strings.Split(ip, ":")[0] | ||
} | ||
|
||
return ip | ||
} | ||
|
||
// IPs show all user signup IPs | ||
func IPs(ctx *context.Context) { | ||
ctx.Data["Title"] = ctx.Tr("admin.ips") | ||
ctx.Data["PageIsAdminIPs"] = true | ||
ctx.Data["RecordUserSignupMetadata"] = setting.RecordUserSignupMetadata | ||
|
||
// If record user signup metadata is disabled, don't show the page | ||
if !setting.RecordUserSignupMetadata { | ||
ctx.Redirect(setting.AppSubURL + "/-/admin") | ||
return | ||
} | ||
|
||
page := ctx.FormInt("page") | ||
if page <= 1 { | ||
page = 1 | ||
} | ||
|
||
// Define the user IP result struct | ||
type UserIPResult struct { | ||
UID int64 | ||
Check failure on line 64 in routers/web/admin/ips.go
|
||
Name string | ||
FullName string | ||
IP string | ||
} | ||
|
||
var ( | ||
userIPs []UserIPResult | ||
count int64 | ||
err error | ||
orderBy string | ||
keyword = ctx.FormTrim("q") | ||
sortType = ctx.FormString("sort") | ||
) | ||
|
||
ctx.Data["SortType"] = sortType | ||
switch sortType { | ||
case "ip": | ||
orderBy = "user_setting.setting_value ASC, user.id ASC" | ||
case "reverseip": | ||
orderBy = "user_setting.setting_value DESC, user.id DESC" | ||
case "username": | ||
orderBy = "user.lower_name ASC, user.id ASC" | ||
case "reverseusername": | ||
orderBy = "user.lower_name DESC, user.id DESC" | ||
default: | ||
ctx.Data["SortType"] = "ip" | ||
sortType = "ip" | ||
Check failure on line 91 in routers/web/admin/ips.go
|
||
orderBy = "user_setting.setting_value ASC, user.id ASC" | ||
} | ||
|
||
// Get the count and user IPs for pagination | ||
if len(keyword) == 0 { | ||
techknowlogick marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Simple count without keyword | ||
count, err = db.GetEngine(ctx). | ||
Join("INNER", "user", "user.id = user_setting.user_id"). | ||
Where("user_setting.setting_key = ?", user_model.SignupIP). | ||
Count(new(user_model.Setting)) | ||
if err != nil { | ||
ctx.ServerError("Count", err) | ||
return | ||
} | ||
|
||
// Get the user IPs | ||
err = db.GetEngine(ctx). | ||
Table("user_setting"). | ||
Join("INNER", "user", "user.id = user_setting.user_id"). | ||
Where("user_setting.setting_key = ?", user_model.SignupIP). | ||
Select("user.id as uid, user.name, user.full_name, user_setting.setting_value as ip, '' as user_agent"). | ||
OrderBy(orderBy). | ||
Limit(setting.UI.Admin.UserPagingNum, (page-1)*setting.UI.Admin.UserPagingNum). | ||
Find(&userIPs) | ||
if err != nil { | ||
ctx.ServerError("Find", err) | ||
return | ||
} | ||
} else { | ||
// Count with keyword filter | ||
count, err = db.GetEngine(ctx). | ||
Join("INNER", "user", "user.id = user_setting.user_id"). | ||
Where("user_setting.setting_key = ?", user_model.SignupIP). | ||
And("(user.lower_name LIKE ? OR user.full_name LIKE ? OR user_setting.setting_value LIKE ?)", | ||
"%"+strings.ToLower(keyword)+"%", "%"+keyword+"%", "%"+keyword+"%"). | ||
Count(new(user_model.Setting)) | ||
if err != nil { | ||
ctx.ServerError("Count", err) | ||
return | ||
} | ||
|
||
// Get the user IPs with keyword filter | ||
err = db.GetEngine(ctx). | ||
Table("user_setting"). | ||
Join("INNER", "user", "user.id = user_setting.user_id"). | ||
Where("user_setting.setting_key = ?", user_model.SignupIP). | ||
And("(user.lower_name LIKE ? OR user.full_name LIKE ? OR user_setting.setting_value LIKE ?)", | ||
"%"+strings.ToLower(keyword)+"%", "%"+keyword+"%", "%"+keyword+"%"). | ||
Select("user.id as uid, user.name, user.full_name, user_setting.setting_value as ip, '' as user_agent"). | ||
OrderBy(orderBy). | ||
Limit(setting.UI.Admin.UserPagingNum, (page-1)*setting.UI.Admin.UserPagingNum). | ||
Find(&userIPs) | ||
if err != nil { | ||
ctx.ServerError("Find", err) | ||
return | ||
} | ||
} | ||
for i := range userIPs { | ||
// Trim the port from the IP | ||
// FIXME: Maybe have a different helper for this? | ||
userIPs[i].IP = trimPortFromIP(userIPs[i].IP) | ||
} | ||
|
||
ctx.Data["UserIPs"] = userIPs | ||
ctx.Data["Total"] = count | ||
ctx.Data["Keyword"] = keyword | ||
|
||
// Setup pagination | ||
ctx.Data["Page"] = context.NewPagination(int(count), setting.UI.Admin.UserPagingNum, page, 5) | ||
|
||
ctx.HTML(http.StatusOK, tplIPs) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}} | ||
<div class="admin-setting-content"> | ||
<h4 class="ui top attached header"> | ||
{{ctx.Locale.Tr "admin.ips.ip_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) | ||
</h4> | ||
<div class="ui attached segment"> | ||
<div class="ui secondary filter menu tw-items-center tw-mx-0"> | ||
<form class="ui form ignore-dirty tw-flex-1"> | ||
{{template "shared/search/combo" dict "Value" .Keyword}} | ||
</form> | ||
<!-- Sort --> | ||
<div class="ui dropdown type jump item tw-mr-0"> | ||
<span class="text"> | ||
{{ctx.Locale.Tr "repo.issues.filter_sort"}} | ||
</span> | ||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||
<div class="menu"> | ||
<a class="{{if or (eq .SortType "ip") (not .SortType)}}active {{end}}item" href="?sort=ip&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.ips.filter_sort.ip"}}</a> | ||
<a class="{{if eq .SortType "reverseip"}}active {{end}}item" href="?sort=reverseip&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.ips.filter_sort.ip_reverse"}}</a> | ||
<a class="{{if eq .SortType "username"}}active {{end}}item" href="?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.ips.filter_sort.name"}}</a> | ||
<a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.ips.filter_sort.name_reverse"}}</a> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="ui attached table segment"> | ||
<table class="ui very basic striped table unstackable"> | ||
<thead> | ||
<tr> | ||
<th data-sortt-asc="username" data-sortt-desc="reverseusername"> | ||
{{ctx.Locale.Tr "admin.users.name"}} | ||
{{SortArrow "username" "reverseusername" $.SortType false}} | ||
</th> | ||
<th>{{ctx.Locale.Tr "admin.users.full_name"}}</th> | ||
<th data-sortt-asc="ip" data-sortt-desc="reverseip" data-sortt-default="true"> | ||
{{ctx.Locale.Tr "admin.ips.ip"}} | ||
{{SortArrow "ip" "reverseip" $.SortType true}} | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{{range .UserIPs}} | ||
<tr> | ||
<td><a href="{{AppSubUrl}}/-/admin/users/{{.UID}}">{{.Name}}</a></td> | ||
<td>{{.FullName}}</td> | ||
<td><a href="?q={{.IP}}&sort={{$.SortType}}">{{.IP}}</a></td> | ||
</tr> | ||
{{else}} | ||
<tr><td class="tw-text-center" colspan="3">{{ctx.Locale.Tr "no_results_found"}}</td></tr> | ||
{{end}} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
{{template "base/paginate" .}} | ||
</div> | ||
|
||
{{template "admin/layout_footer" .}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{{if .HasSignupIP}} | ||
<div class="flex-list"> | ||
<div class="flex-item"> | ||
<div class="flex-item-main"> | ||
<div class="flex-text-block"> | ||
<strong>{{ctx.Locale.Tr "admin.ips.ip"}}:</strong> <a href="{{AppSubUrl}}/-/admin/ips?q={{.SignupIP}}">{{.SignupIP}}</a> | ||
</div> | ||
{{if .HasSignupUserAgent}} | ||
<div class="flex-text-block"> | ||
<strong>{{ctx.Locale.Tr "admin.ips.user_agent"}}:</strong> {{.SignupUserAgent}} | ||
</div> | ||
{{end}} | ||
</div> | ||
</div> | ||
</div> | ||
{{else}} | ||
<div>{{ctx.Locale.Tr "admin.ips.not_available"}}</div> | ||
{{end}} |
Uh oh!
There was an error while loading. Please reload this page.