mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-03 17:25:10 +01:00
Merge c0f910e322afa4c60207e44395246584dcb7b6e0 into 98ff7d077376db1225f266095788c6bd9414288a
This commit is contained in:
commit
9a86efee86
72
PACKAGES_EXPLORE_FEATURE.md
Normal file
72
PACKAGES_EXPLORE_FEATURE.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Packages Explore Feature
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This feature adds a new "Packages" tab to the Explore page, allowing users to discover and browse packages that they have access to across the Gitea instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### User-Facing Features
|
||||||
|
1. **Packages Tab in Explore**: A new tab in the explore navigation that displays all accessible packages
|
||||||
|
2. **Search and Filter**: Users can search packages by name and filter by package type (npm, Maven, Docker, etc.)
|
||||||
|
3. **Permission-Based Access**: Only shows packages that the user has permission to view based on:
|
||||||
|
- Public user packages (visible to everyone)
|
||||||
|
- Limited visibility user packages (visible to logged-in users)
|
||||||
|
- Organization packages (visible based on org visibility and membership)
|
||||||
|
- Private packages (only visible to the owner)
|
||||||
|
|
||||||
|
### Admin Features
|
||||||
|
1. **Toggle Control**: Admins can enable/disable the packages explore page via `app.ini` configuration
|
||||||
|
2. **Configuration Setting**: `[service.explore]` section with `DISABLE_PACKAGES_PAGE` option
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add the following to your `app.ini` file under the `[service.explore]` section:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[service.explore]
|
||||||
|
; Disable the packages explore page
|
||||||
|
DISABLE_PACKAGES_PAGE = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Set to `true` to hide the packages tab from the explore page.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Backend Changes
|
||||||
|
- **New Handler**: `routers/web/explore/packages.go` - Handles package listing with permission filtering
|
||||||
|
- **Configuration**: `modules/setting/service.go` - Added `DisablePackagesPage` setting
|
||||||
|
- **Route**: Added `/explore/packages` route in `routers/web/web.go`
|
||||||
|
|
||||||
|
### Frontend Changes
|
||||||
|
- **Template**: `templates/explore/packages.tmpl` - Displays package list with search/filter
|
||||||
|
- **Navigation**: Updated `templates/explore/navbar.tmpl` to include packages tab
|
||||||
|
|
||||||
|
### Permission Logic
|
||||||
|
The feature implements proper access control by:
|
||||||
|
1. Fetching packages from the database
|
||||||
|
2. Checking each package's owner visibility:
|
||||||
|
- For user-owned packages: Check user visibility (public/limited/private)
|
||||||
|
- For org-owned packages: Check org visibility and user membership
|
||||||
|
3. Filtering results to only show accessible packages
|
||||||
|
4. Respecting the `DISABLE_PACKAGES_PAGE` configuration setting
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
- Anonymous users only see packages from public users/organizations
|
||||||
|
- Logged-in users see packages from public and limited visibility users, plus organizations they're members of
|
||||||
|
- Private user packages are only visible to the owner
|
||||||
|
- The feature requires packages to be enabled (`[packages] ENABLED = true`)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
To test the feature:
|
||||||
|
1. Enable packages in your Gitea instance
|
||||||
|
2. Create packages under different users/organizations with varying visibility settings
|
||||||
|
3. Access `/explore/packages` as different user types (anonymous, logged-in, org member)
|
||||||
|
4. Verify that only appropriate packages are displayed
|
||||||
|
5. Test the admin toggle by setting `DISABLE_PACKAGES_PAGE = true` and verifying the tab disappears
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
Potential improvements for future versions:
|
||||||
|
- Add sorting options (by date, name, downloads)
|
||||||
|
- Implement more efficient database-level permission filtering
|
||||||
|
- Add package statistics and trending packages
|
||||||
|
- Support for package categories/tags
|
||||||
@ -946,6 +946,9 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;; Disable the code explore page.
|
;; Disable the code explore page.
|
||||||
;DISABLE_CODE_PAGE = false
|
;DISABLE_CODE_PAGE = false
|
||||||
|
;;
|
||||||
|
;; Disable the packages explore page.
|
||||||
|
;DISABLE_PACKAGES_PAGE = false
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -97,6 +97,7 @@ var Service = struct {
|
|||||||
DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"`
|
DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"`
|
||||||
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
||||||
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
||||||
|
DisablePackagesPage bool `ini:"DISABLE_PACKAGES_PAGE"`
|
||||||
} `ini:"service.explore"`
|
} `ini:"service.explore"`
|
||||||
|
|
||||||
QoS struct {
|
QoS struct {
|
||||||
|
|||||||
@ -30,6 +30,8 @@ func Code(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||||
|
ctx.Data["PackagesPageIsDisabled"] = setting.Service.Explore.DisablePackagesPage
|
||||||
|
ctx.Data["PackagesEnabled"] = setting.Packages.Enabled
|
||||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||||
ctx.Data["Title"] = ctx.Tr("explore")
|
ctx.Data["Title"] = ctx.Tr("explore")
|
||||||
ctx.Data["PageIsExplore"] = true
|
ctx.Data["PageIsExplore"] = true
|
||||||
|
|||||||
@ -22,6 +22,8 @@ func Organizations(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||||
|
ctx.Data["PackagesPageIsDisabled"] = setting.Service.Explore.DisablePackagesPage
|
||||||
|
ctx.Data["PackagesEnabled"] = setting.Packages.Enabled
|
||||||
ctx.Data["Title"] = ctx.Tr("explore")
|
ctx.Data["Title"] = ctx.Tr("explore")
|
||||||
ctx.Data["PageIsExplore"] = true
|
ctx.Data["PageIsExplore"] = true
|
||||||
ctx.Data["PageIsExploreOrganizations"] = true
|
ctx.Data["PageIsExploreOrganizations"] = true
|
||||||
|
|||||||
124
routers/web/explore/packages.go
Normal file
124
routers/web/explore/packages.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package explore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
org_model "code.gitea.io/gitea/models/organization"
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplExplorePackages templates.TplName = "explore/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Packages render explore packages page
|
||||||
|
func Packages(ctx *context.Context) {
|
||||||
|
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||||
|
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||||
|
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||||
|
ctx.Data["PackagesPageIsDisabled"] = setting.Service.Explore.DisablePackagesPage
|
||||||
|
ctx.Data["PackagesEnabled"] = setting.Packages.Enabled
|
||||||
|
ctx.Data["Title"] = ctx.Tr("explore")
|
||||||
|
ctx.Data["PageIsExplore"] = true
|
||||||
|
ctx.Data["PageIsExplorePackages"] = true
|
||||||
|
|
||||||
|
page := ctx.FormInt("page")
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
query := ctx.FormTrim("q")
|
||||||
|
packageType := ctx.FormTrim("type")
|
||||||
|
|
||||||
|
ctx.Data["Query"] = query
|
||||||
|
ctx.Data["PackageType"] = packageType
|
||||||
|
ctx.Data["AvailableTypes"] = packages_model.TypeList
|
||||||
|
|
||||||
|
// Get all packages matching the search criteria
|
||||||
|
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
|
Paginator: &db.ListOptions{
|
||||||
|
PageSize: setting.UI.PackagesPagingNum * 3, // Get more to account for filtering
|
||||||
|
Page: page,
|
||||||
|
},
|
||||||
|
Type: packages_model.Type(packageType),
|
||||||
|
Name: packages_model.SearchValue{Value: query},
|
||||||
|
IsInternal: optional.Some(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("SearchLatestVersions", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter packages based on user permissions
|
||||||
|
accessiblePVs := make([]*packages_model.PackageVersion, 0, len(pvs))
|
||||||
|
for _, pv := range pvs {
|
||||||
|
pkg, err := packages_model.GetPackageByID(ctx, pv.PackageID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetPackageByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, err := user_model.GetUserByID(ctx, pkg.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has access to this package based on owner visibility
|
||||||
|
hasAccess := false
|
||||||
|
if owner.IsOrganization() {
|
||||||
|
// For organizations, check if user can see the org
|
||||||
|
if ctx.Doer != nil {
|
||||||
|
isMember, err := org_model.IsOrganizationMember(ctx, owner.ID, ctx.Doer.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsOrganizationMember", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasAccess = isMember || owner.Visibility == structs.VisibleTypePublic
|
||||||
|
} else {
|
||||||
|
hasAccess = owner.Visibility == structs.VisibleTypePublic
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For users, check visibility
|
||||||
|
if ctx.Doer != nil {
|
||||||
|
hasAccess = owner.Visibility == structs.VisibleTypePublic ||
|
||||||
|
owner.Visibility == structs.VisibleTypeLimited ||
|
||||||
|
owner.ID == ctx.Doer.ID
|
||||||
|
} else {
|
||||||
|
hasAccess = owner.Visibility == structs.VisibleTypePublic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAccess {
|
||||||
|
accessiblePVs = append(accessiblePVs, pv)
|
||||||
|
if len(accessiblePVs) >= setting.UI.PackagesPagingNum {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pds, err := packages_model.GetPackageDescriptors(ctx, accessiblePVs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetPackageDescriptors", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Total"] = int64(len(accessiblePVs))
|
||||||
|
ctx.Data["PackageDescriptors"] = pds
|
||||||
|
|
||||||
|
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
|
||||||
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplExplorePackages)
|
||||||
|
}
|
||||||
@ -149,6 +149,8 @@ func Repos(ctx *context.Context) {
|
|||||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||||
|
ctx.Data["PackagesPageIsDisabled"] = setting.Service.Explore.DisablePackagesPage
|
||||||
|
ctx.Data["PackagesEnabled"] = setting.Packages.Enabled
|
||||||
ctx.Data["Title"] = ctx.Tr("explore")
|
ctx.Data["Title"] = ctx.Tr("explore")
|
||||||
ctx.Data["PageIsExplore"] = true
|
ctx.Data["PageIsExplore"] = true
|
||||||
ctx.Data["ShowRepoOwnerOnList"] = true
|
ctx.Data["ShowRepoOwnerOnList"] = true
|
||||||
|
|||||||
@ -134,6 +134,8 @@ func Users(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||||
|
ctx.Data["PackagesPageIsDisabled"] = setting.Service.Explore.DisablePackagesPage
|
||||||
|
ctx.Data["PackagesEnabled"] = setting.Packages.Enabled
|
||||||
ctx.Data["Title"] = ctx.Tr("explore")
|
ctx.Data["Title"] = ctx.Tr("explore")
|
||||||
ctx.Data["PageIsExplore"] = true
|
ctx.Data["PageIsExplore"] = true
|
||||||
ctx.Data["PageIsExploreUsers"] = true
|
ctx.Data["PageIsExploreUsers"] = true
|
||||||
|
|||||||
@ -508,6 +508,7 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}, explore.Code)
|
}, explore.Code)
|
||||||
|
m.Get("/packages", packagesEnabled, explore.Packages)
|
||||||
m.Get("/topics/search", explore.TopicSearch)
|
m.Get("/topics/search", explore.TopicSearch)
|
||||||
}, optExploreSignIn)
|
}, optExploreSignIn)
|
||||||
|
|
||||||
|
|||||||
@ -18,5 +18,10 @@
|
|||||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "explore.code"}}
|
{{svg "octicon-code"}} {{ctx.Locale.Tr "explore.code"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if and .PackagesEnabled (not .PackagesPageIsDisabled)}}
|
||||||
|
<a class="{{if .PageIsExplorePackages}}active {{end}}item" href="{{AppSubUrl}}/explore/packages">
|
||||||
|
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</overflow-menu>
|
</overflow-menu>
|
||||||
|
|||||||
50
templates/explore/packages.tmpl
Normal file
50
templates/explore/packages.tmpl
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{{template "base/head" .}}
|
||||||
|
<div role="main" aria-label="{{.Title}}" class="page-content explore packages">
|
||||||
|
{{template "explore/navbar" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
<form class="ui form ignore-dirty">
|
||||||
|
<div class="ui small fluid action input">
|
||||||
|
{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
|
||||||
|
<select class="ui small dropdown" name="type">
|
||||||
|
<option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option>
|
||||||
|
<option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option>
|
||||||
|
{{range $type := .AvailableTypes}}
|
||||||
|
<option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
{{template "shared/search/button"}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
{{range .PackageDescriptors}}
|
||||||
|
<div class="flex-list">
|
||||||
|
<div class="flex-item">
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
|
||||||
|
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-body">
|
||||||
|
{{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}}
|
||||||
|
{{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{if eq .Total 0}}
|
||||||
|
<div class="empty-placeholder">
|
||||||
|
{{svg "octicon-package" 48}}
|
||||||
|
<h2>{{ctx.Locale.Tr "packages.empty"}}</h2>
|
||||||
|
<p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}</p>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{template "base/paginate" .}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
||||||
Loading…
x
Reference in New Issue
Block a user