mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 02:04:11 +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_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
 | 
			
		||||
 | 
			
		||||
@ -508,6 +508,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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user