AI Gen inital commit

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
Misha Dowd 2025-10-20 21:46:29 +00:00
parent b2ee5be52e
commit c0f910e322
11 changed files with 264 additions and 0 deletions

View 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

View File

@ -946,6 +946,9 @@ LEVEL = Info
;;
;; Disable the code explore page.
;DISABLE_CODE_PAGE = false
;;
;; Disable the packages explore page.
;DISABLE_PACKAGES_PAGE = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -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 {

View File

@ -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

View File

@ -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

View 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)
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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>

View 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" .}}