mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: add admin views for federation configuration, hosts and users (#11115)
Fixes #9282 Adds a new admin panel category for federation related administration. Includes views for: - Instance Federation Configuration - List of Federation Hosts - (Per-Instance) List of Federated Users Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11115 Reviewed-by: elle <0xllx0@noreply.codeberg.org> Reviewed-by: Panagiotis "Ivory" Vasilopoulos <git@n0toose.net> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Florian Pallas <mail@fpallas.com> Co-committed-by: Florian Pallas <mail@fpallas.com>
This commit is contained in:
parent
65044ca765
commit
4e6a782a89
21 changed files with 973 additions and 12 deletions
|
|
@ -16,6 +16,31 @@ func init() {
|
|||
db.RegisterModel(new(FederationHost))
|
||||
}
|
||||
|
||||
func CountFederationHosts(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Count(FederationHost{})
|
||||
}
|
||||
|
||||
func FindFederationHosts(ctx context.Context, opts db.ListOptions) (hosts []*FederationHost, err error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if opts.PageSize > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
|
||||
err = sess.Find(&hosts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
if res, err := validation.IsValid(host); !res {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) {
|
||||
log.Trace("GetFederationHost: %v", ID)
|
||||
host := new(FederationHost)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,57 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID
|
|||
return user, federatedUser, nil
|
||||
}
|
||||
|
||||
func CountFederatedUsers(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Count(FederatedUser{})
|
||||
}
|
||||
|
||||
func FindFederatedUsers(ctx context.Context, opts db.ListOptions) (users []*FederatedUser, err error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if opts.PageSize > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
|
||||
err = sess.Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if res, err := validation.IsValid(user); !res {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, err
|
||||
}
|
||||
|
||||
func CountFederatedUsersByHostID(ctx context.Context, federationHostID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID).Count(FederatedUser{})
|
||||
}
|
||||
|
||||
func FindFederatedUsersByHostID(ctx context.Context, federationHostID int64, opts db.ListOptions) ([]*FederatedUser, error) {
|
||||
var users []*FederatedUser
|
||||
sess := db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID)
|
||||
|
||||
if opts.PageSize > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
|
||||
err := sess.Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if res, err := validation.IsValid(user); !res {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
|
||||
user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -55,10 +55,12 @@ var UI = struct {
|
|||
} `ini:"ui.csv"`
|
||||
|
||||
Admin struct {
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
FederationHostPagingNum int
|
||||
FederationUserPagingNum int
|
||||
} `ini:"ui.admin"`
|
||||
User struct {
|
||||
RepoPagingNum int
|
||||
|
|
@ -114,15 +116,19 @@ var UI = struct {
|
|||
MaxRows: 2500,
|
||||
},
|
||||
Admin: struct {
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
UserPagingNum int
|
||||
RepoPagingNum int
|
||||
NoticePagingNum int
|
||||
OrgPagingNum int
|
||||
FederationHostPagingNum int
|
||||
FederationUserPagingNum int
|
||||
}{
|
||||
UserPagingNum: 50,
|
||||
RepoPagingNum: 50,
|
||||
NoticePagingNum: 25,
|
||||
OrgPagingNum: 50,
|
||||
UserPagingNum: 50,
|
||||
RepoPagingNum: 50,
|
||||
NoticePagingNum: 25,
|
||||
OrgPagingNum: 50,
|
||||
FederationHostPagingNum: 50,
|
||||
FederationUserPagingNum: 50,
|
||||
},
|
||||
User: struct {
|
||||
RepoPagingNum int
|
||||
|
|
|
|||
|
|
@ -144,6 +144,37 @@
|
|||
"keys.ssh.link": "SSH keys",
|
||||
"keys.gpg.link": "GPG keys",
|
||||
"keys.verify.token.hint": "The token is only valid for 1 minute. <a href=\"%[1]s\">Get a new one if it expired</a>.",
|
||||
"admin.federation.federation": "Federation",
|
||||
"admin.federation.hosts": "Hosts",
|
||||
"admin.federation.hosts.title": "Federation hosts",
|
||||
"admin.federation.hosts.manage_panel": "Manage federation hosts",
|
||||
"admin.federation.hosts.details_panel": "Federation host details",
|
||||
"admin.federation.hosts.show_details": "Show host details",
|
||||
"admin.federation.host.id": "ID",
|
||||
"admin.federation.host.fqdn": "FQDN",
|
||||
"admin.federation.host.schema": "Schema",
|
||||
"admin.federation.host.port": "Port",
|
||||
"admin.federation.host.software_name": "Software",
|
||||
"admin.federation.host.created": "Created",
|
||||
"admin.federation.host.updated": "Updated",
|
||||
"admin.federation.host.latest_activity": "Latest activity",
|
||||
"admin.federation.users": "Users",
|
||||
"admin.federation.users.title": "Federated users",
|
||||
"admin.federation.users.manage_panel": "Manage federated users",
|
||||
"admin.federation.users.show_local_user": "Show local user details",
|
||||
"admin.federation.user.id": "ID",
|
||||
"admin.federation.user.user_id": "Local user ID",
|
||||
"admin.federation.user.external_id": "External user ID",
|
||||
"admin.federation.user.inbox_path": "Inbox path",
|
||||
"admin.config.federation": "Federation configuration",
|
||||
"admin.config.federation.enabled": "Enabled",
|
||||
"admin.config.federation.share_user_statistics": "Share user statistics with other hosts",
|
||||
"admin.config.federation.max_size": "Max allowed response size",
|
||||
"admin.config.federation.signature_enforced": "Require HTTP signatures",
|
||||
"admin.config.federation.signature_algorithms": "Signature algorithms",
|
||||
"admin.config.federation.digest_algorithm": "Signature digest algorithm",
|
||||
"admin.config.federation.get_headers": "Signed GET headers",
|
||||
"admin.config.federation.post_headers": "Signed POST headers",
|
||||
"admin.config.moderation_config": "Moderation configuration",
|
||||
"admin.moderation.moderation_reports": "Moderation reports",
|
||||
"admin.moderation.reports": "Reports",
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ func Config(ctx *context.Context) {
|
|||
ctx.Data["DbCfg"] = setting.Database
|
||||
ctx.Data["Webhook"] = setting.Webhook
|
||||
ctx.Data["Moderation"] = setting.Moderation
|
||||
ctx.Data["Federation"] = setting.Federation
|
||||
ctx.Data["FederationMaxSize"] = setting.Federation.MaxSize / 1024 / 1024 // in MiB
|
||||
|
||||
ctx.Data["MailerEnabled"] = false
|
||||
if setting.MailService != nil {
|
||||
|
|
|
|||
65
routers/web/admin/federation_host.go
Normal file
65
routers/web/admin/federation_host.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/base"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplFederationHost base.TplName = "admin/federation/host"
|
||||
)
|
||||
|
||||
func FederationHost(ctx *context.Context) {
|
||||
federationHostID := ctx.ParamsInt64("id")
|
||||
page := ctx.FormInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
host, err := forgefed.GetFederationHost(ctx, federationHostID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetFederationHost", err)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := user_model.FindFederatedUsersByHostID(ctx, federationHostID, db.ListOptions{
|
||||
PageSize: setting.UI.Admin.FederationUserPagingNum,
|
||||
Page: int(page),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindFederatedUsersByHostID", err)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := user_model.CountFederatedUsersByHostID(ctx, federationHostID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountFederatedUsersByHostID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Host"] = host
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["UsersTotal"] = int(total)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.federation.hosts.details_panel")
|
||||
ctx.Data["PageIsAdminFederationHosts"] = true
|
||||
|
||||
numPages := 0
|
||||
if total > 0 {
|
||||
numPages = (int(total) - 1/setting.UI.Admin.FederationUserPagingNum)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.Admin.FederationUserPagingNum, page, numPages)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplFederationHost)
|
||||
}
|
||||
58
routers/web/admin/federation_hosts.go
Normal file
58
routers/web/admin/federation_hosts.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/modules/base"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplFederationHosts base.TplName = "admin/federation/hosts"
|
||||
)
|
||||
|
||||
func FederationHosts(ctx *context.Context) {
|
||||
sort := ctx.FormTrim("sort")
|
||||
page := ctx.FormInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
hosts, err := forgefed.FindFederationHosts(ctx, db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.FederationHostPagingNum,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetFederationHosts", err)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := forgefed.CountFederationHosts(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountFederationHosts", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("admin.federation.hosts.title")
|
||||
ctx.Data["PageIsAdminFederationHosts"] = true
|
||||
ctx.Data["SortType"] = sort
|
||||
ctx.Data["TotalCount"] = int(total)
|
||||
ctx.Data["Hosts"] = hosts
|
||||
|
||||
numPages := 0
|
||||
if total > 0 {
|
||||
numPages = (int(total) - 1/setting.UI.Admin.FederationHostPagingNum)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.Admin.FederationHostPagingNum, page, numPages)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplFederationHosts)
|
||||
}
|
||||
56
routers/web/admin/federation_users.go
Normal file
56
routers/web/admin/federation_users.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/base"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplFederationUsers base.TplName = "admin/federation/users"
|
||||
)
|
||||
|
||||
func FederationUsers(ctx *context.Context) {
|
||||
page := ctx.FormInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
users, err := user_model.FindFederatedUsers(ctx, db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.FederationUserPagingNum,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindFederatedUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
total, err := user_model.CountFederatedUsers(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountFederatedUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["TotalCount"] = int(total)
|
||||
ctx.Data["Title"] = ctx.Tr("admin.federation.users.title")
|
||||
ctx.Data["PageIsAdminFederationUsers"] = true
|
||||
|
||||
numPages := 0
|
||||
if total > 0 {
|
||||
numPages = (int(total) - 1/setting.UI.Admin.FederationUserPagingNum)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.Admin.FederationUserPagingNum, page, numPages)
|
||||
pager.SetDefaultParams(ctx)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplFederationUsers)
|
||||
}
|
||||
|
|
@ -837,6 +837,14 @@ func registerRoutes(m *web.Route) {
|
|||
})
|
||||
m.Post("/abuse_reports/act", admin.PerformAction)
|
||||
}
|
||||
|
||||
if setting.Federation.Enabled {
|
||||
m.Group("/federation", func() {
|
||||
m.Get("/hosts", admin.FederationHosts)
|
||||
m.Get("/users", admin.FederationUsers)
|
||||
m.Get("/hosts/{id}", admin.FederationHost)
|
||||
})
|
||||
}
|
||||
}, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableModeration", setting.Moderation.Enabled))
|
||||
// ***** END: Admin *****
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
||||
ctx.Data["DisableForks"] = setting.Repository.DisableForks
|
||||
ctx.Data["EnableActions"] = setting.Actions.Enabled
|
||||
ctx.Data["EnableFederation"] = setting.Federation.Enabled
|
||||
|
||||
ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
|
||||
ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
|
||||
|
|
|
|||
|
|
@ -344,6 +344,33 @@
|
|||
</dl>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.config.federation"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.enabled"}}</dt>
|
||||
<dd>{{if .Federation.Enabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.share_user_statistics"}}</dt>
|
||||
<dd>{{if .Federation.ShareUserStatistics}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.max_size"}}</dt>
|
||||
<dd>{{.FederationMaxSize}} MiB</dd>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.signature_enforced"}}</dt>
|
||||
<dd>{{if .Federation.SignatureEnforced}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.signature_algorithms"}}</dt>
|
||||
<dd>{{.Federation.SignatureAlgorithms}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.digest_algorithm"}}</dt>
|
||||
<dd>{{.Federation.DigestAlgorithm}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.get_headers"}}</dt>
|
||||
<dd>{{.Federation.GetHeaders}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.federation.post_headers"}}</dt>
|
||||
<dd>{{.Federation.PostHeaders}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.config.log_config"}}
|
||||
</h4>
|
||||
|
|
|
|||
39
templates/admin/federation/host.tmpl
Normal file
39
templates/admin/federation/host.tmpl
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
|
||||
<div class="admin-setting-content">
|
||||
{{if .Host}}
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.federation.hosts.details_panel"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.id"}}</dt>
|
||||
<dd>{{.Host.ID}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.fqdn"}}</dt>
|
||||
<dd>{{.Host.HostFqdn}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.port"}}</dt>
|
||||
<dd>{{.Host.HostPort}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.schema"}}</dt>
|
||||
<dd>{{.Host.HostSchema}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.software_name"}}</dt>
|
||||
<dd>{{.Host.NodeInfo.SoftwareName}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.created"}}</dt>
|
||||
<dd>{{DateUtils.AbsoluteShort .Host.Created}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.updated"}}</dt>
|
||||
<dd>{{DateUtils.AbsoluteShort .Host.Updated}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.federation.host.latest_activity"}}</dt>
|
||||
<dd>{{DateUtils.FullTime .Host.LatestActivity}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{{end}}
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.federation.users.manage_panel"}}
|
||||
<div class="ui right">
|
||||
{{.UsersTotal}}
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
{{template "admin/federation/user_list" .}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
56
templates/admin/federation/hosts.tmpl
Normal file
56
templates/admin/federation/hosts.tmpl
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.federation.hosts.manage_panel"}} ({{ctx.Locale.Tr "admin.total" .TotalCount}})
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="id_asc" data-sortt-desc="id_desc">
|
||||
{{ctx.Locale.Tr "admin.federation.host.id"}}
|
||||
{{SortArrow "id_asc" "id_desc" .SortType true}}
|
||||
</th>
|
||||
<th data-sortt-asc="fqdn_asc" data-sortt-desc="fqdn_desc">
|
||||
{{ctx.Locale.Tr "admin.federation.host.fqdn"}}
|
||||
{{SortArrow "fqdn_asc" "fqdn_desc" .SortType false}}
|
||||
</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.host.schema"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.host.port"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.host.software_name"}}</th>
|
||||
<th data-sortt-asc="created_asc" data-sortt-desc="created_desc">
|
||||
{{ctx.Locale.Tr "admin.federation.host.created"}}
|
||||
{{SortArrow "created_asc" "created_desc" .SortType true}}
|
||||
</th>
|
||||
<th data-sortt-asc="activity_asc" data-sortt-desc="activity_desc">
|
||||
{{ctx.Locale.Tr "admin.federation.host.latest_activity"}}
|
||||
{{SortArrow "activity_asc" "activity_desc" .SortType true}}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Hosts}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.HostFqdn}}</td>
|
||||
<td>{{.HostSchema}}</td>
|
||||
<td>{{.HostPort}}</td>
|
||||
<td>{{.NodeInfo.SoftwareName}}</td>
|
||||
<td>{{DateUtils.AbsoluteShort .Created}}</td>
|
||||
<td>{{DateUtils.FullTime .LatestActivity}}</td>
|
||||
<td>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.federation.hosts.show_details"}}">{{svg "octicon-server"}}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "repo.pulls.no_results"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
28
templates/admin/federation/user_list.tmpl
Normal file
28
templates/admin/federation/user_list.tmpl
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<table class="ui very basic striped table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.user.id"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.user.user_id"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.user.external_id"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.federation.user.inbox_path"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.UserID}}</td>
|
||||
<td>{{.ExternalID}}</td>
|
||||
<td>{{.InboxPath}}</td>
|
||||
<td>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<a href="{{AppUrl}}admin/users/{{.UserID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.federation.users.show_local_user"}}">{{svg "octicon-person"}}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "repo.pulls.no_results"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
11
templates/admin/federation/users.tmpl
Normal file
11
templates/admin/federation/users.tmpl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.federation.users.manage_panel"}} ({{ctx.Locale.Tr "admin.total" .TotalCount}})
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
{{template "admin/federation/user_list" .}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
|
@ -77,6 +77,19 @@
|
|||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
{{if .EnableFederation}}
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminFederationHosts .PageIsAdminFederationUsers}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.federation.federation"}}</summary>
|
||||
<div class="menu">
|
||||
<a class="{{if .PageIsAdminFederationHosts}}active {{end}}item" href="{{AppSubUrl}}/admin/federation/hosts">
|
||||
{{ctx.Locale.Tr "admin.federation.hosts"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsAdminFederationUsers}}active {{end}}item" href="{{AppSubUrl}}/admin/federation/users">
|
||||
{{ctx.Locale.Tr "admin.federation.users"}}
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||
<div class="menu">
|
||||
|
|
|
|||
128
tests/integration/admin_federation_test.go
Normal file
128
tests/integration/admin_federation_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/routers"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAdminFederationViewHostsAndUsers(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestAdminFederationViewHostsAndUsers")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Federation enabled", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
t.Run("Anonymous user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
MakeRequest(t, req, http.StatusSeeOther)
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
MakeRequest(t, req, http.StatusSeeOther)
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
MakeRequest(t, req, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Normal user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("Admin user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
hostRows := htmlDoc.Find(".admin-setting-content table tbody tr")
|
||||
assert.Equal(t, 2, hostRows.Length())
|
||||
assert.Contains(t, hostRows.Text(), "bob.example.com")
|
||||
assert.Contains(t, hostRows.Text(), "alice.example.com")
|
||||
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
userRows := htmlDoc.Find(".admin-setting-content table tbody tr")
|
||||
assert.Equal(t, 3, userRows.Length())
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/1/inbox#bob")
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/1/inbox#alice")
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/2/inbox#eve")
|
||||
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
userRows = htmlDoc.Find(".admin-setting-content table tbody tr")
|
||||
assert.Equal(t, 1, userRows.Length())
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/1/inbox#bob")
|
||||
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/2")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
userRows = htmlDoc.Find(".admin-setting-content table tbody tr")
|
||||
assert.Equal(t, 2, userRows.Length())
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/1/inbox#alice")
|
||||
assert.Contains(t, userRows.Text(), "/api/v1/activitypub/user-id/2/inbox#eve")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Federation disabled", func(t *testing.T) {
|
||||
t.Run("Anonymous user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Normal user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Admin user", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
req := NewRequest(t, "GET", "/admin/federation/hosts")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/hosts/1")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/admin/federation/users")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
- id: 1
|
||||
user_id: 1001
|
||||
external_id: "1"
|
||||
federation_host_id: 1
|
||||
inbox_path: /api/v1/activitypub/user-id/1/inbox#bob
|
||||
|
||||
- id: 2
|
||||
user_id: 1002
|
||||
external_id: "1"
|
||||
federation_host_id: 2
|
||||
inbox_path: /api/v1/activitypub/user-id/1/inbox#alice
|
||||
|
||||
- id: 3
|
||||
user_id: 1003
|
||||
external_id: "2"
|
||||
federation_host_id: 2
|
||||
inbox_path: /api/v1/activitypub/user-id/2/inbox#eve
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
- id: 1
|
||||
host_fqdn: bob.example.com
|
||||
host_port: 80
|
||||
host_schema: http
|
||||
software_name: forgejo
|
||||
latest_activity: 2023-01-01T00:00:00Z
|
||||
|
||||
- id: 2
|
||||
host_fqdn: alice.example.com
|
||||
host_port: 443
|
||||
host_schema: https
|
||||
software_name: gitea
|
||||
latest_activity: 2023-01-01T00:00:00Z
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
- id: 1001
|
||||
lower_name: "@bob@bob.example.com"
|
||||
name: "@bob@bob.example.com"
|
||||
full_name: "@bob@bob.example.com"
|
||||
email: bob@bob.example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: ZogKvWdyEx:password
|
||||
passwd_hash_algo: dummy
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: bob
|
||||
type: 0
|
||||
salt: ZogKvWdyEx
|
||||
max_repo_creation: -1
|
||||
is_active: true
|
||||
is_admin: false
|
||||
is_restricted: false
|
||||
allow_git_hook: false
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar1001
|
||||
avatar_email: bob@bob.example.com
|
||||
use_custom_avatar: false
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 0
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
||||
|
||||
- id: 1002
|
||||
lower_name: "@alice@alice.example.com"
|
||||
name: "@alice@alice.example.com"
|
||||
full_name: "@alice@alice.example.com"
|
||||
email: alice@alice.example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: ZogKvWdyEx:password
|
||||
passwd_hash_algo: dummy
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: alice
|
||||
type: 0
|
||||
salt: ZogKvWdyEx
|
||||
max_repo_creation: -1
|
||||
is_active: true
|
||||
is_admin: false
|
||||
is_restricted: false
|
||||
allow_git_hook: false
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar1002
|
||||
avatar_email: alice@alice.example.com
|
||||
use_custom_avatar: false
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 0
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
||||
|
||||
- id: 1003
|
||||
lower_name: "@eve@alice.example.com"
|
||||
name: "@eve@alice.example.com"
|
||||
full_name: "@eve@alice.example.com"
|
||||
email: eve@alice.example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: ZogKvWdyEx:password
|
||||
passwd_hash_algo: dummy
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: eve
|
||||
type: 0
|
||||
salt: ZogKvWdyEx
|
||||
max_repo_creation: -1
|
||||
is_active: true
|
||||
is_admin: false
|
||||
is_restricted: false
|
||||
allow_git_hook: false
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar1003
|
||||
avatar_email: eve@alice.example.com
|
||||
use_custom_avatar: false
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 0
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
||||
219
tests/integration/repo_forgefed_test.go
Normal file
219
tests/integration/repo_forgefed_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/routers"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestForgefedRepositoryCreateHostValid(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Arrange
|
||||
ctx := t.Context()
|
||||
|
||||
// Act
|
||||
err := forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
HostFqdn: "forgejo.example.com",
|
||||
HostPort: 80,
|
||||
HostSchema: "http",
|
||||
NodeInfo: forgefed.NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
})
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "forgejo.example.com"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestForgefedRepositoryCreateHostInvalid(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Arrange
|
||||
ctx := t.Context()
|
||||
|
||||
// Act
|
||||
err := forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
// invalid
|
||||
})
|
||||
|
||||
// Assert
|
||||
require.Error(t, err)
|
||||
unittest.AssertNotExistsBean(t, &forgefed.FederationHost{HostFqdn: "forgejo.example.com"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestForgefedRepositoryCreateUserValid(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Arrange
|
||||
ctx := t.Context()
|
||||
|
||||
err := forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
HostFqdn: "forgejo.example.com",
|
||||
HostPort: 80,
|
||||
HostSchema: "http",
|
||||
NodeInfo: forgefed.NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Act
|
||||
err = user.CreateFederatedUser(ctx, &user.User{
|
||||
Name: "@bob@forgejo.example.com",
|
||||
Email: "bob@forgejo.example.com",
|
||||
}, &user.FederatedUser{
|
||||
ExternalID: "1",
|
||||
FederationHostID: 1,
|
||||
InboxPath: "/inbox",
|
||||
})
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
localUser := unittest.AssertExistsAndLoadBean(t, &user.User{Name: "@bob@forgejo.example.com", Email: "bob@forgejo.example.com"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{UserID: localUser.ID, FederationHostID: 1})
|
||||
})
|
||||
}
|
||||
|
||||
func TestForgefedRepositoryCreateUserInvalid(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Arrange
|
||||
ctx := t.Context()
|
||||
|
||||
err := forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
HostFqdn: "forgejo.example.com",
|
||||
HostPort: 80,
|
||||
HostSchema: "http",
|
||||
NodeInfo: forgefed.NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Act
|
||||
err = user.CreateFederatedUser(ctx, &user.User{
|
||||
Name: "@bob@forgejo.example.com",
|
||||
Email: "bob@forgejo.example.com",
|
||||
}, &user.FederatedUser{
|
||||
// invalid
|
||||
})
|
||||
|
||||
// Assert
|
||||
require.Error(t, err)
|
||||
unittest.AssertNotExistsBean(t, &user.User{Email: "bob@forgejo.example.com"})
|
||||
unittest.AssertNotExistsBean(t, &user.FederatedUser{FederationHostID: 1})
|
||||
})
|
||||
}
|
||||
|
||||
func TestForgefedRepositoryFindHostsAndUsers(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Arrange
|
||||
ctx := t.Context()
|
||||
|
||||
err := forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
HostFqdn: "bob.example.com",
|
||||
HostPort: 80,
|
||||
HostSchema: "http",
|
||||
NodeInfo: forgefed.NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = forgefed.CreateFederationHost(ctx, &forgefed.FederationHost{
|
||||
HostFqdn: "alice.example.com",
|
||||
HostPort: 443,
|
||||
HostSchema: "https",
|
||||
NodeInfo: forgefed.NodeInfo{
|
||||
SoftwareName: "gitea",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user.CreateFederatedUser(ctx, &user.User{
|
||||
Name: "@bob@bob.example.com",
|
||||
Email: "bob@bob.example.com",
|
||||
}, &user.FederatedUser{
|
||||
ExternalID: "1",
|
||||
FederationHostID: 1,
|
||||
InboxPath: "/inbox",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user.CreateFederatedUser(ctx, &user.User{
|
||||
Name: "@alice@alice.example.com",
|
||||
Email: "alice@alice.example.com",
|
||||
}, &user.FederatedUser{
|
||||
ExternalID: "1",
|
||||
FederationHostID: 2,
|
||||
InboxPath: "/inbox",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user.CreateFederatedUser(ctx, &user.User{
|
||||
Name: "@eve@alice.example.com",
|
||||
Email: "eve@alice.example.com",
|
||||
}, &user.FederatedUser{
|
||||
ExternalID: "2",
|
||||
FederationHostID: 2,
|
||||
InboxPath: "/inbox",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Act & Assert
|
||||
hosts, err := forgefed.FindFederationHosts(ctx, db.ListOptions{PageSize: 100, Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, hosts, 2)
|
||||
hostFqdns := []string{hosts[0].HostFqdn, hosts[1].HostFqdn}
|
||||
assert.Contains(t, hostFqdns, "bob.example.com")
|
||||
assert.Contains(t, hostFqdns, "alice.example.com")
|
||||
|
||||
count, err := forgefed.CountFederationHosts(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
|
||||
users, err := user.FindFederatedUsers(ctx, db.ListOptions{PageSize: 100, Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 3) // Bob, Alice and Eve
|
||||
|
||||
count, err = user.CountFederatedUsers(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
|
||||
users, err = user.FindFederatedUsersByHostID(ctx, 1, db.ListOptions{PageSize: 100, Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 1) // Only Bob belongs to the host with ID 1
|
||||
assert.Equal(t, int64(1), users[0].FederationHostID)
|
||||
|
||||
count, err = user.CountFederatedUsersByHostID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
users, err = user.FindFederatedUsersByHostID(ctx, 2, db.ListOptions{PageSize: 100, Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 2) // Alice and Eve belong to the host with ID 2
|
||||
assert.Equal(t, int64(2), users[0].FederationHostID)
|
||||
assert.Equal(t, int64(2), users[1].FederationHostID)
|
||||
|
||||
count, err = user.CountFederatedUsersByHostID(ctx, 2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue