Compare commits

...

4 Commits

Author SHA1 Message Date
Misha Dowd
eb866a6c7d
Merge c0f910e322 into bc50431e8b 2025-10-27 00:06:57 +08:00
Lunny Xiao
bc50431e8b
Upgrade go mail to 0.7.2 (#35748) 2025-10-26 09:52:01 -04:00
GiteaBot
2a6af15448 [skip ci] Updated translations via Crowdin 2025-10-26 00:38:59 +00:00
Misha Dowd
c0f910e322 AI Gen inital commit
Co-authored-by: Ona <no-reply@ona.com>
2025-10-20 21:46:29 +00:00
41 changed files with 295 additions and 3 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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

2
go.mod
View File

@ -109,7 +109,7 @@ require (
github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.4.1
github.com/wneessen/go-mail v0.7.1
github.com/wneessen/go-mail v0.7.2
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.13

4
go.sum
View File

@ -768,8 +768,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk=
github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=

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

@ -3586,6 +3586,7 @@ variables.update.success=Proměnná byla upravena.
logs.always_auto_scroll=Vždy automaticky posouvat logy
logs.always_expand_running=Vždy rozšířit běžící logy
[projects]
deleted.display_name=Odstraněný projekt
type-1.display_name=Samostatný projekt

View File

@ -3645,6 +3645,7 @@ variables.update.success=Die Variable wurde bearbeitet.
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
logs.always_expand_running=Laufende Logs immer erweitern
[projects]
deleted.display_name=Gelöschtes Projekt
type-1.display_name=Individuelles Projekt

View File

@ -3280,6 +3280,7 @@ variables.update.failed=Αποτυχία επεξεργασίας μεταβλη
variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
[projects]
type-1.display_name=Ατομικό Έργο
type-2.display_name=Έργο Αποθετηρίου

View File

@ -3257,6 +3257,7 @@ variables.update.failed=Error al editar la variable.
variables.update.success=La variable ha sido editada.
[projects]
type-1.display_name=Proyecto individual
type-2.display_name=Proyecto repositorio

View File

@ -2446,6 +2446,7 @@ runs.commit=کامیت
[projects]
[git.filemode]

View File

@ -1693,6 +1693,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3906,6 +3906,7 @@ variables.update.success=La variable a bien été modifiée.
logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement
logs.always_expand_running=Toujours développer les journaux en cours
[projects]
deleted.display_name=Projet supprimé
type-1.display_name=Projet personnel

View File

@ -3914,6 +3914,7 @@ variables.update.success=Tá an t-athróg curtha in eagar.
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
[projects]
deleted.display_name=Tionscadal scriosta
type-1.display_name=Tionscadal Aonair

View File

@ -1605,6 +1605,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -1428,6 +1428,7 @@ variables.update.failed=Gagal mengedit variabel.
variables.update.success=Variabel telah diedit.
[projects]
type-1.display_name=Proyek Individu
type-2.display_name=Proyek Repositori

View File

@ -1334,6 +1334,7 @@ runs.commit=Framlag
[projects]
[git.filemode]

View File

@ -2706,6 +2706,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3910,6 +3910,7 @@ variables.update.success=変数を更新しました。
logs.always_auto_scroll=常にログを自動スクロール
logs.always_expand_running=常に実行中のログを展開
[projects]
deleted.display_name=削除されたプロジェクト
type-1.display_name=個人プロジェクト

View File

@ -1554,6 +1554,7 @@ runs.commit=커밋
[projects]
[git.filemode]

View File

@ -3282,6 +3282,7 @@ variables.update.failed=Neizdevās labot mainīgo.
variables.update.success=Mainīgais tika labots.
[projects]
type-1.display_name=Individuālais projekts
type-2.display_name=Repozitorija projekts

View File

@ -2458,6 +2458,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -2347,6 +2347,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3615,6 +3615,7 @@ variables.update.failed=Falha ao editar a variável.
variables.update.success=A variável foi editada.
[projects]
deleted.display_name=Excluir Projeto
type-1.display_name=Projeto Individual

View File

@ -3914,6 +3914,7 @@ variables.update.success=A variável foi editada.
logs.always_auto_scroll=Rolar registos de forma automática e permanente
logs.always_expand_running=Expandir sempre os registos que vão rolando
[projects]
deleted.display_name=Planeamento eliminado
type-1.display_name=Planeamento individual

View File

@ -3225,6 +3225,7 @@ variables.update.failed=Не удалось изменить переменну
variables.update.success=Переменная изменена.
[projects]
type-1.display_name=Индивидуальный проект
type-2.display_name=Проект репозитория

View File

@ -2391,6 +2391,7 @@ runs.commit=කැප
[projects]
[git.filemode]

View File

@ -1292,6 +1292,7 @@ runners.labels=Štítky
[projects]
[git.filemode]

View File

@ -1968,6 +1968,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3907,6 +3907,7 @@ variables.update.success=Değişken düzenlendi.
logs.always_auto_scroll=Günlükleri her zaman otomatik kaydır
logs.always_expand_running=Çalıştırma günlüklerini her zaman genişlet
[projects]
deleted.display_name=Silinmiş Proje
type-1.display_name=Kişisel Proje

View File

@ -3428,6 +3428,7 @@ variables.update.success=Змінну відредаговано.
logs.always_auto_scroll=Завжди автоматично прокручувати журнали
logs.always_expand_running=Завжди розгортати поточні журнали
[projects]
deleted.display_name=Видалений проєкт
type-1.display_name=Індивідуальний проєкт

View File

@ -3911,6 +3911,7 @@ variables.update.success=变量已编辑。
logs.always_auto_scroll=总是自动滚动日志
logs.always_expand_running=总是展开运行日志
[projects]
deleted.display_name=已删除项目
type-1.display_name=个人项目

View File

@ -980,6 +980,7 @@ runners.task_list.repository=儲存庫
[projects]
[git.filemode]

View File

@ -3554,6 +3554,7 @@ variables.update.failed=編輯變數失敗。
variables.update.success=已編輯變數。
[projects]
deleted.display_name=已刪除的專案
type-1.display_name=個人專案

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