{{ctx.Locale.Tr "packages.empty"}}
+{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}
+{{ctx.Locale.Tr "packages.filter.no_result"}}
+ {{end}} + {{end}} + {{template "base/paginate" .}} +diff --git a/PACKAGES_EXPLORE_FEATURE.md b/PACKAGES_EXPLORE_FEATURE.md new file mode 100644 index 0000000000..7736fa9580 --- /dev/null +++ b/PACKAGES_EXPLORE_FEATURE.md @@ -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 diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5fee78af54..d90633e2ce 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -946,6 +946,9 @@ LEVEL = Info ;; ;; Disable the code explore page. ;DISABLE_CODE_PAGE = false +;; +;; Disable the packages explore page. +;DISABLE_PACKAGES_PAGE = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/service.go b/modules/setting/service.go index e652c13c9c..648e5ebd44 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -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 { diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 3bb50ef397..d20e7bc5f6 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -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 diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index 4d25f4ec2d..d070a71e18 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -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 diff --git a/routers/web/explore/packages.go b/routers/web/explore/packages.go new file mode 100644 index 0000000000..5b3d1940cd --- /dev/null +++ b/routers/web/explore/packages.go @@ -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) +} diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 1b269deb98..1bc6944bd3 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -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 diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 4b3c269410..4bd8607e0a 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -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 diff --git a/routers/web/web.go b/routers/web/web.go index 8b55e4469e..7637ca62be 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/templates/explore/navbar.tmpl b/templates/explore/navbar.tmpl index 6b595af63a..9f8024368b 100644 --- a/templates/explore/navbar.tmpl +++ b/templates/explore/navbar.tmpl @@ -18,5 +18,10 @@ {{svg "octicon-code"}} {{ctx.Locale.Tr "explore.code"}} {{end}} + {{if and .PackagesEnabled (not .PackagesPageIsDisabled)}} + + {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} + + {{end}} diff --git a/templates/explore/packages.tmpl b/templates/explore/packages.tmpl new file mode 100644 index 0000000000..1e07414eeb --- /dev/null +++ b/templates/explore/packages.tmpl @@ -0,0 +1,50 @@ +{{template "base/head" .}} +
{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}
+{{ctx.Locale.Tr "packages.filter.no_result"}}
+ {{end}} + {{end}} + {{template "base/paginate" .}} +