mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-14 19:07:08 +01:00
Merge 6476a7b775ce212f4265994c73671bfb6491fc88 into 8aa1179ce45daff794ffb3754bdc3c828b394262
This commit is contained in:
commit
3c3c69ef85
@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setting is a key value store of user settings
|
// Setting is a key value store of user settings
|
||||||
@ -211,3 +212,17 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string
|
|||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|||||||
32
modules/util/network.go
Normal file
32
modules/util/network.go
Normal file
@ -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
|
||||||
|
}
|
||||||
66
modules/util/network_test.go
Normal file
66
modules/util/network_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3145,6 +3145,16 @@ users.list_status_filter.is_2fa_enabled = 2FA Enabled
|
|||||||
users.list_status_filter.not_2fa_enabled = 2FA Disabled
|
users.list_status_filter.not_2fa_enabled = 2FA Disabled
|
||||||
users.details = User Details
|
users.details = User Details
|
||||||
|
|
||||||
|
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.email_manage_panel = User Email Management
|
||||||
emails.primary = Primary
|
emails.primary = Primary
|
||||||
emails.activated = Activated
|
emails.activated = Activated
|
||||||
|
|||||||
104
routers/web/admin/ips.go
Normal file
104
routers/web/admin/ips.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplIPs templates.TplName = "admin/ips/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IPs show all user signup IPs
|
||||||
|
func IPs(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.ips.ip")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
orderBy = "user_setting.setting_value ASC, user.id ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the count and user IPs for pagination
|
||||||
|
query := user_model.BuildSignupIPQuery(ctx, keyword)
|
||||||
|
|
||||||
|
count, err = query.Count(new(user_model.Setting))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Count", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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).
|
||||||
|
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 = util.TrimPortFromIP(userIPs[i].IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["UserIPs"] = userIPs
|
||||||
|
ctx.Data["Total"] = count
|
||||||
|
ctx.Data["Keyword"] = keyword
|
||||||
|
|
||||||
|
// Setup pagination
|
||||||
|
pager := context.NewPagination(int(count), setting.UI.Admin.UserPagingNum, page, 5)
|
||||||
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplIPs)
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/web/explore"
|
"code.gitea.io/gitea/routers/web/explore"
|
||||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||||
@ -263,6 +264,7 @@ func ViewUser(ctx *context.Context) {
|
|||||||
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
|
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
|
||||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||||
|
ctx.Data["ShowUserSignupMetadata"] = setting.RecordUserSignupMetadata
|
||||||
|
|
||||||
u := prepareUserInfo(ctx)
|
u := prepareUserInfo(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
@ -292,6 +294,25 @@ func ViewUser(ctx *context.Context) {
|
|||||||
ctx.Data["Emails"] = emails
|
ctx.Data["Emails"] = emails
|
||||||
ctx.Data["EmailsTotal"] = len(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"] = util.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{
|
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
|
||||||
ListOptions: db.ListOptionsAll,
|
ListOptions: db.ListOptionsAll,
|
||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
|
|||||||
@ -760,6 +760,10 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Post("/delete", admin.DeleteEmail)
|
m.Post("/delete", admin.DeleteEmail)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Group("/ips", func() {
|
||||||
|
m.Get("", admin.IPs)
|
||||||
|
})
|
||||||
|
|
||||||
m.Group("/orgs", func() {
|
m.Group("/orgs", func() {
|
||||||
m.Get("", admin.Organizations)
|
m.Get("", admin.Organizations)
|
||||||
})
|
})
|
||||||
@ -819,7 +823,7 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
addSettingsRunnersRoutes()
|
addSettingsRunnersRoutes()
|
||||||
addSettingsVariablesRoutes()
|
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 *****
|
// ***** END: Admin *****
|
||||||
|
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
|
|||||||
58
templates/admin/ips/list.tmpl
Normal file
58
templates/admin/ips/list.tmpl
Normal file
@ -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" .}}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
|
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminIPs .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
|
||||||
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
|
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
|
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
|
||||||
@ -28,6 +28,11 @@
|
|||||||
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
|
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
|
||||||
{{ctx.Locale.Tr "admin.emails"}}
|
{{ctx.Locale.Tr "admin.emails"}}
|
||||||
</a>
|
</a>
|
||||||
|
{{if .RecordUserSignupMetadata}}
|
||||||
|
<a class="{{if .PageIsAdminIPs}}active {{end}}item" href="{{AppSubUrl}}/-/admin/ips">
|
||||||
|
{{ctx.Locale.Tr "admin.ips.ip"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details class="item toggleable-item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>
|
<details class="item toggleable-item" {{if or .PageIsAdminRepositories (and .EnablePackages .PageIsAdminPackages)}}open{{end}}>
|
||||||
|
|||||||
@ -22,6 +22,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .ShowUserSignupMetadata}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "admin.ips.signup_metadata"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{template "admin/user/view_ip" .}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<h4 class="ui top attached header">
|
<h4 class="ui top attached header">
|
||||||
{{ctx.Locale.Tr "admin.repositories"}} ({{ctx.Locale.Tr "admin.total" .ReposTotal}})
|
{{ctx.Locale.Tr "admin.repositories"}} ({{ctx.Locale.Tr "admin.total" .ReposTotal}})
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
18
templates/admin/user/view_ip.tmpl
Normal file
18
templates/admin/user/view_ip.tmpl
Normal file
@ -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}}
|
||||||
Loading…
x
Reference in New Issue
Block a user