From 5d716fbe0b568db3b81a5048702ca602ecfa176e Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 19:31:40 -0400 Subject: [PATCH] Display Usersign metadata in admin dashboard --- options/locale/locale_en-US.ini | 11 ++ routers/web/admin/ips.go | 164 ++++++++++++++++++++++++++++++ routers/web/admin/users.go | 20 ++++ routers/web/web.go | 6 +- templates/admin/ips/list.tmpl | 58 +++++++++++ templates/admin/navbar.tmpl | 7 +- templates/admin/user/view.tmpl | 8 ++ templates/admin/user/view_ip.tmpl | 18 ++++ 8 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 routers/web/admin/ips.go create mode 100644 templates/admin/ips/list.tmpl create mode 100644 templates/admin/user/view_ip.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 876e135b22..eb43140094 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3075,6 +3075,17 @@ users.list_status_filter.is_2fa_enabled = 2FA Enabled users.list_status_filter.not_2fa_enabled = 2FA Disabled users.details = User Details +ips = Signup IPs +ips.ip = IP Address +ips.user_agent = User Agent +ips.ip_manage_panel = Signup IP Management +ips.signup_metadata = Signup Metadata +ips.not_available = Signup metadata not available +ips.filter_sort.ip = Sort by IP (asc) +ips.filter_sort.ip_reverse = Sort by IP (desc) +ips.filter_sort.name = Sort by Username (asc) +ips.filter_sort.name_reverse = Sort by Username (desc) + emails.email_manage_panel = User Email Management emails.primary = Primary emails.activated = Activated diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go new file mode 100644 index 0000000000..6656ed8b0a --- /dev/null +++ b/routers/web/admin/ips.go @@ -0,0 +1,164 @@ +// 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 + Name string + FullName string + IP string + UserAgent 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" + orderBy = "user_setting.setting_value ASC, user.id ASC" + } + + // Get the count and user IPs for pagination + if len(keyword) == 0 { + // 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) +} diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index f6a3af1c86..7673fe6ace 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -263,6 +263,7 @@ func ViewUser(ctx *context.Context) { ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() + ctx.Data["ShowUserSignupMetadata"] = setting.RecordUserSignupMetadata u := prepareUserInfo(ctx) if ctx.Written() { @@ -292,6 +293,25 @@ func ViewUser(ctx *context.Context) { ctx.Data["Emails"] = emails ctx.Data["EmailsTotal"] = len(emails) + // If record user signup metadata is enabled, get the user's signup IP and user agent + if setting.RecordUserSignupMetadata { + signupIP, err := user_model.GetUserSetting(ctx, u.ID, user_model.SignupIP) + if err == nil && len(signupIP) > 0 { + ctx.Data["HasSignupIP"] = true + ctx.Data["SignupIP"] = trimPortFromIP(signupIP) + } else { + ctx.Data["HasSignupIP"] = false + } + + signupUserAgent, err := user_model.GetUserSetting(ctx, u.ID, user_model.SignupUserAgent) + if err == nil && len(signupUserAgent) > 0 { + ctx.Data["HasSignupUserAgent"] = true + ctx.Data["SignupUserAgent"] = signupUserAgent + } else { + ctx.Data["HasSignupUserAgent"] = false + } + } + orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{ ListOptions: db.ListOptionsAll, UserID: u.ID, diff --git a/routers/web/web.go b/routers/web/web.go index f4bd3ef4bc..5fff811acd 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -757,6 +757,10 @@ func registerRoutes(m *web.Router) { m.Post("/delete", admin.DeleteEmail) }) + m.Group("/ips", func() { + m.Get("", admin.IPs) + }) + m.Group("/orgs", func() { m.Get("", admin.Organizations) }) @@ -816,7 +820,7 @@ func registerRoutes(m *web.Router) { addSettingsRunnersRoutes() addSettingsVariablesRoutes() }) - }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled)) + }, adminReq, ctxDataSet("RecordUserSignupMetadata", setting.RecordUserSignupMetadata, "EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled)) // ***** END: Admin ***** m.Group("", func() { diff --git a/templates/admin/ips/list.tmpl b/templates/admin/ips/list.tmpl new file mode 100644 index 0000000000..95e105fa98 --- /dev/null +++ b/templates/admin/ips/list.tmpl @@ -0,0 +1,58 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}} +
+

+ {{ctx.Locale.Tr "admin.ips.ip_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) +

+
+ +
+
+ + + + + + + + + + {{range .UserIPs}} + + + + + + {{else}} + + {{end}} + +
+ {{ctx.Locale.Tr "admin.users.name"}} + {{SortArrow "username" "reverseusername" $.SortType false}} + {{ctx.Locale.Tr "admin.users.full_name"}} + {{ctx.Locale.Tr "admin.ips.ip"}} + {{SortArrow "ip" "reverseip" $.SortType true}} +
{{.Name}}{{.FullName}}{{.IP}}
{{ctx.Locale.Tr "no_results_found"}}
+
+ + {{template "base/paginate" .}} +
+ +{{template "admin/layout_footer" .}} \ No newline at end of file diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 72584ec799..2f008c6cbb 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -13,7 +13,7 @@ -
+
{{ctx.Locale.Tr "admin.identity_access"}}
diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl index 21943a8382..bf82db5d3b 100644 --- a/templates/admin/user/view.tmpl +++ b/templates/admin/user/view.tmpl @@ -25,6 +25,14 @@ + {{if .ShowUserSignupMetadata}} +

+ {{ctx.Locale.Tr "admin.ips.signup_metadata"}} +

+
+ {{template "admin/user/view_ip" .}} +
+ {{end}}

{{ctx.Locale.Tr "admin.repositories"}}
diff --git a/templates/admin/user/view_ip.tmpl b/templates/admin/user/view_ip.tmpl new file mode 100644 index 0000000000..0306f6d6a9 --- /dev/null +++ b/templates/admin/user/view_ip.tmpl @@ -0,0 +1,18 @@ +{{if .HasSignupIP}} +
+
+
+
+ {{ctx.Locale.Tr "admin.ips.ip"}}: {{.SignupIP}} +
+ {{if .HasSignupUserAgent}} +
+ {{ctx.Locale.Tr "admin.ips.user_agent"}}: {{.SignupUserAgent}} +
+ {{end}} +
+
+
+{{else}} +
{{ctx.Locale.Tr "admin.ips.not_available"}}
+{{end}} \ No newline at end of file