mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
AI Gen inital commit
Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
parent
b2ee5be52e
commit
c0f910e322
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_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"`
|
||||
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
||||
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
||||
DisablePackagesPage bool `ini:"DISABLE_PACKAGES_PAGE"`
|
||||
} `ini:"service.explore"`
|
||||
|
||||
QoS struct {
|
||||
|
||||
@ -30,6 +30,8 @@ func Code(ctx *context.Context) {
|
||||
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
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["Title"] = ctx.Tr("explore")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
|
||||
@ -22,6 +22,8 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
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["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["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["ShowRepoOwnerOnList"] = true
|
||||
|
||||
@ -134,6 +134,8 @@ func Users(ctx *context.Context) {
|
||||
}
|
||||
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["PageIsExploreUsers"] = true
|
||||
|
||||
@ -512,6 +512,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
return
|
||||
}
|
||||
}, explore.Code)
|
||||
m.Get("/packages", packagesEnabled, explore.Packages)
|
||||
m.Get("/topics/search", explore.TopicSearch)
|
||||
}, optExploreSignIn)
|
||||
|
||||
|
||||
@ -18,5 +18,10 @@
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "explore.code"}}
|
||||
</a>
|
||||
{{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>
|
||||
</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…
Reference in New Issue
Block a user