diff --git a/models/user/setting.go b/models/user/setting.go index b4af0e5ccd..6e9d9a3ea6 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 0000000000..e918f4a7dd --- /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 0000000000..e254c9d23a --- /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 61a0a873b5..08fd9d7a41 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 7673fe6ace..74813e8d54 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 }