From 5d716fbe0b568db3b81a5048702ca602ecfa176e Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 19:31:40 -0400 Subject: [PATCH 01/12] 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 876e135b22f57..eb431400942f6 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 0000000000000..6656ed8b0a441 --- /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 f6a3af1c866d4..7673fe6acef49 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 f4bd3ef4bce99..5fff811acd8cc 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 0000000000000..95e105fa98518 --- /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 72584ec799cc3..2f008c6cbb4c6 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 21943a8382dc3..bf82db5d3b677 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 0000000000000..0306f6d6a9a37 --- /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 From 996fae02c36c005d4d20e7bb408912f53a87a99a Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 20:00:38 -0400 Subject: [PATCH 02/12] Update routers/web/admin/ips.go --- routers/web/admin/ips.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index 6656ed8b0a441..ff338d332e791 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -65,7 +65,6 @@ func IPs(ctx *context.Context) { Name string FullName string IP string - UserAgent string } var ( From 7888a5aaf34cefdd646c3c6d03efebd3b3d165c3 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 21:13:18 -0400 Subject: [PATCH 03/12] reduce code duplication due to queries --- routers/web/admin/ips.go | 79 +++++++++++++++------------------------- 1 file changed, 29 insertions(+), 50 deletions(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index 6656ed8b0a441..a5cedd12be72b 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" + "xorm.io/xorm" ) const ( @@ -42,6 +43,19 @@ func trimPortFromIP(ip string) string { return ip } +func buildIPQuery(ctx *context.Context, keyword string) *xorm.Session { + query := db.GetEngine(ctx). + Table("user_setting"). + Join("INNER", "user", "user.id = user_setting.user_id"). + Where("user_setting.setting_key = ?", user_model.SignupIP) + + if len(keyword) > 0 { + query = query.And("(user.lower_name LIKE ? OR user.full_name LIKE ? OR user_setting.setting_value LIKE ?)", + "%"+strings.ToLower(keyword)+"%", "%"+keyword+"%", "%"+keyword+"%") + } + return query +} + // IPs show all user signup IPs func IPs(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.ips") @@ -94,59 +108,24 @@ func IPs(ctx *context.Context) { } // 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 - } + query := buildIPQuery(ctx, keyword) - // 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 - } + count, err = query.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 - } + err = buildIPQuery(ctx, keyword). + Select("user.id as uid, user.name, user.full_name, user_setting.setting_value as ip"). + 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? From abebe115db18d33bbb5627c944d3a1c4526183ca Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 22:06:33 -0400 Subject: [PATCH 04/12] make fmt --- routers/web/admin/ips.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index 6e762d3906ca7..af432b170460c 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" + "xorm.io/xorm" ) @@ -75,10 +76,10 @@ func IPs(ctx *context.Context) { // Define the user IP result struct type UserIPResult struct { - UID int64 - Name string - FullName string - IP string + UID int64 + Name string + FullName string + IP string } var ( From ec44bdb5e7ac310b1af516ccba8bb304f1190334 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 20 Mar 2025 22:38:18 -0400 Subject: [PATCH 05/12] whoops. left in an extra assign --- routers/web/admin/ips.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index af432b170460c..c16724c46cc2a 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -103,7 +103,6 @@ func IPs(ctx *context.Context) { orderBy = "user.lower_name DESC, user.id DESC" default: ctx.Data["SortType"] = "ip" - sortType = "ip" orderBy = "user_setting.setting_value ASC, user.id ASC" } From 2fcb0cb8a787dedd7231ddb5c82deea3dcb793c5 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Fri, 21 Mar 2025 02:52:46 -0400 Subject: [PATCH 06/12] add final newline --- templates/admin/ips/list.tmpl | 2 +- templates/admin/user/view_ip.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/ips/list.tmpl b/templates/admin/ips/list.tmpl index 95e105fa98518..6124b59950f07 100644 --- a/templates/admin/ips/list.tmpl +++ b/templates/admin/ips/list.tmpl @@ -55,4 +55,4 @@ {{template "base/paginate" .}}
-{{template "admin/layout_footer" .}} \ No newline at end of file +{{template "admin/layout_footer" .}} diff --git a/templates/admin/user/view_ip.tmpl b/templates/admin/user/view_ip.tmpl index 0306f6d6a9a37..b8b163a3040c7 100644 --- a/templates/admin/user/view_ip.tmpl +++ b/templates/admin/user/view_ip.tmpl @@ -15,4 +15,4 @@ {{else}}
{{ctx.Locale.Tr "admin.ips.not_available"}}
-{{end}} \ No newline at end of file +{{end}} From b1b8d9bdd433eebf34acf69ac5c475b8bd0cf9ef Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 23 Mar 2025 10:15:53 -0400 Subject: [PATCH 07/12] Update locale_en-US.ini --- options/locale/locale_en-US.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eb431400942f6..5110d5e4337b9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3075,7 +3075,6 @@ 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 From f26cb7cefa50423b6fe52707963e099d14c36d04 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 23 Mar 2025 10:19:10 -0400 Subject: [PATCH 08/12] Update navbar.tmpl --- templates/admin/navbar.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 2f008c6cbb4c6..1b89a965ea5d4 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -30,7 +30,7 @@ {{if .RecordUserSignupMetadata}} - {{ctx.Locale.Tr "admin.ips"}} + {{ctx.Locale.Tr "admin.ips.ip"}} {{end}} From bf9fc6c6b120ff5d44826c48b7466d4f28c92f1c Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 23 Mar 2025 10:20:09 -0400 Subject: [PATCH 09/12] Update ips.go --- routers/web/admin/ips.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index c16724c46cc2a..61a0a873b5017 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -59,7 +59,7 @@ func buildIPQuery(ctx *context.Context, keyword string) *xorm.Session { // IPs show all user signup IPs func IPs(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("admin.ips") + ctx.Data["Title"] = ctx.Tr("admin.ips.ip") ctx.Data["PageIsAdminIPs"] = true ctx.Data["RecordUserSignupMetadata"] = setting.RecordUserSignupMetadata From 03af3b88c4259225bd7ffc3b64ed02fee0158271 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 10 Apr 2025 14:52:11 -0400 Subject: [PATCH 10/12] move helper funcs --- models/user/setting.go | 15 ++++++++ modules/util/network.go | 32 +++++++++++++++++ modules/util/network_test.go | 66 ++++++++++++++++++++++++++++++++++++ routers/web/admin/ips.go | 48 +++----------------------- routers/web/admin/users.go | 2 +- 5 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 modules/util/network.go create mode 100644 modules/util/network_test.go diff --git a/models/user/setting.go b/models/user/setting.go index b4af0e5ccd684..6e9d9a3ea6e64 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm" ) // Setting is a key value store of user settings @@ -210,3 +211,17 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string return err }) } + +// BuildSignupIPQuery builds a query to find users by their signup IP addresses +func BuildSignupIPQuery(ctx context.Context, keyword string) *xorm.Session { + query := db.GetEngine(ctx). + Table("user_setting"). + Join("INNER", "user", "user.id = user_setting.user_id"). + Where("user_setting.setting_key = ?", SignupIP) + + if len(keyword) > 0 { + query = query.And("(user.lower_name LIKE ? OR user.full_name LIKE ? OR user_setting.setting_value LIKE ?)", + "%"+strings.ToLower(keyword)+"%", "%"+keyword+"%", "%"+keyword+"%") + } + return query +} diff --git a/modules/util/network.go b/modules/util/network.go new file mode 100644 index 0000000000000..e918f4a7ddfe6 --- /dev/null +++ b/modules/util/network.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "strings" +) + +// 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 +} diff --git a/modules/util/network_test.go b/modules/util/network_test.go new file mode 100644 index 0000000000000..e254c9d23a015 --- /dev/null +++ b/modules/util/network_test.go @@ -0,0 +1,66 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTrimPortFromIP(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "IPv4 without port", + input: "192.168.1.1", + expected: "192.168.1.1", + }, + { + name: "IPv4 with port", + input: "192.168.1.1:8080", + expected: "192.168.1.1", + }, + { + name: "IPv6 without port", + input: "2001:db8::1", + expected: "2001:db8::1", + }, + { + name: "IPv6 with brackets, without port", + input: "[2001:db8::1]", + expected: "[2001:db8::1]", + }, + { + name: "IPv6 with brackets and port", + input: "[2001:db8::1]:8080", + expected: "[2001:db8::1]", + }, + { + name: "localhost with port", + input: "localhost:8080", + expected: "localhost", + }, + { + name: "Empty string", + input: "", + expected: "", + }, + { + name: "Not an IP address", + input: "abc123", + expected: "abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TrimPortFromIP(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index 61a0a873b5017..08fd9d7a41bfc 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -5,58 +5,18 @@ 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/modules/util" "code.gitea.io/gitea/services/context" - - "xorm.io/xorm" ) 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 -} - -func buildIPQuery(ctx *context.Context, keyword string) *xorm.Session { - query := db.GetEngine(ctx). - Table("user_setting"). - Join("INNER", "user", "user.id = user_setting.user_id"). - Where("user_setting.setting_key = ?", user_model.SignupIP) - - if len(keyword) > 0 { - query = query.And("(user.lower_name LIKE ? OR user.full_name LIKE ? OR user_setting.setting_value LIKE ?)", - "%"+strings.ToLower(keyword)+"%", "%"+keyword+"%", "%"+keyword+"%") - } - return query -} - // IPs show all user signup IPs func IPs(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.ips.ip") @@ -107,7 +67,7 @@ func IPs(ctx *context.Context) { } // Get the count and user IPs for pagination - query := buildIPQuery(ctx, keyword) + query := user_model.BuildSignupIPQuery(ctx, keyword) count, err = query.Count(new(user_model.Setting)) if err != nil { @@ -115,7 +75,7 @@ func IPs(ctx *context.Context) { return } - err = buildIPQuery(ctx, keyword). + err = user_model.BuildSignupIPQuery(ctx, keyword). Select("user.id as uid, user.name, user.full_name, user_setting.setting_value as ip"). OrderBy(orderBy). Limit(setting.UI.Admin.UserPagingNum, (page-1)*setting.UI.Admin.UserPagingNum). @@ -128,7 +88,7 @@ func IPs(ctx *context.Context) { 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) + userIPs[i].IP = util.TrimPortFromIP(userIPs[i].IP) } ctx.Data["UserIPs"] = userIPs diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 7673fe6acef49..74813e8d54d43 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -298,7 +298,7 @@ func ViewUser(ctx *context.Context) { 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) + ctx.Data["SignupIP"] = util.TrimPortFromIP(signupIP) } else { ctx.Data["HasSignupIP"] = false } From f45ff5ab31a99a2f9d3d3d607fcbb10c3c8fc205 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 10 Apr 2025 14:59:57 -0400 Subject: [PATCH 11/12] add missing import --- routers/web/admin/users.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 389d95744f94d..5ba92a0496e30 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" user_setting "code.gitea.io/gitea/routers/web/user/setting" From 6476a7b775ce212f4265994c73671bfb6491fc88 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 17 Apr 2025 15:58:12 -0400 Subject: [PATCH 12/12] fix pagination in admin dashboard --- routers/web/admin/ips.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/web/admin/ips.go b/routers/web/admin/ips.go index 08fd9d7a41bfc..3ba605630388c 100644 --- a/routers/web/admin/ips.go +++ b/routers/web/admin/ips.go @@ -96,7 +96,9 @@ func IPs(ctx *context.Context) { ctx.Data["Keyword"] = keyword // Setup pagination - ctx.Data["Page"] = context.NewPagination(int(count), setting.UI.Admin.UserPagingNum, page, 5) + pager := context.NewPagination(int(count), setting.UI.Admin.UserPagingNum, page, 5) + pager.AddParamFromRequest(ctx.Req) + ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplIPs) }