mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-11 09:06:19 +00:00
feat(api): add project APIs (#38691)
Adds REST APIs for project boards for repo, org and user scopes, using as much shared code as possible for all 3 scopes. Fixes: https://github.com/go-gitea/gitea/issues/14299 Fixes: https://github.com/go-gitea/gitea/issues/31769 Fixes: https://github.com/go-gitea/gitea/issues/35921 Replaces: https://github.com/go-gitea/gitea/pull/37518 Replaces: https://github.com/go-gitea/gitea/pull/36008 Replaces: https://github.com/go-gitea/gitea/pull/28111 Replaces: https://github.com/go-gitea/gitea/pull/31768 Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: Supen.Huang <supen.huang@qq.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ember <ember@mubergacres.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: beardev-in <abhinav.edulakanti@gmail.com>
This commit is contained in:
@@ -88,6 +88,7 @@ import (
|
||||
"gitea.dev/routers/api/v1/packages"
|
||||
"gitea.dev/routers/api/v1/repo"
|
||||
"gitea.dev/routers/api/v1/settings"
|
||||
"gitea.dev/routers/api/v1/shared"
|
||||
"gitea.dev/routers/api/v1/token"
|
||||
"gitea.dev/routers/api/v1/user"
|
||||
"gitea.dev/routers/common"
|
||||
@@ -799,6 +800,67 @@ func mustEnableWiki(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// reqProjectsUnitAccess mirrors the web's reqUnitAccess for the Projects unit. Org
|
||||
// visibility is too permissive for reads, org ownership too strict for writes.
|
||||
func reqProjectsUnitAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
// "/users/{username}/projects" also accepts an organization, where checkTokenPublicOnly
|
||||
// does nothing because IsTokenAccessAllowed is false for orgs. Enforce it here, before
|
||||
// the admin bypass, so both spellings of the route answer alike.
|
||||
if ctx.PublicOnly && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
if ctx.IsUserSiteAdmin() {
|
||||
return
|
||||
}
|
||||
// individual visibility is handled by individualPermsChecker
|
||||
if ctx.ContextUser.IsOrganization() &&
|
||||
organization.OrgFromUser(ctx.ContextUser).UnitPermission(ctx, ctx.Doer, unit.TypeProjects) < accessMode {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addProjectRoutes registers a scope's project tree, "writeChecks" guard every mutation.
|
||||
func addProjectRoutes(m *web.Router, writeChecks ...any) {
|
||||
m.Get("", shared.ListProjects)
|
||||
m.Group("/{id}", func() {
|
||||
m.Get("", shared.GetProject)
|
||||
m.Get("/columns", shared.ListProjectColumns)
|
||||
m.Group("/columns/{column_id}", func() {
|
||||
m.Get("", shared.GetProjectColumn)
|
||||
m.Get("/issues", shared.ListProjectColumnIssues)
|
||||
})
|
||||
})
|
||||
m.Group("", func() {
|
||||
m.Post("", bind(api.CreateProjectOption{}), shared.CreateProject)
|
||||
m.Group("/{id}", func() {
|
||||
m.Patch("", bind(api.EditProjectOption{}), shared.EditProject)
|
||||
m.Delete("", shared.DeleteProject)
|
||||
m.Post("/columns", bind(api.CreateProjectColumnOption{}), shared.CreateProjectColumn)
|
||||
m.Post("/columns/move", bind(api.MoveProjectColumnsOption{}), shared.MoveProjectColumns)
|
||||
m.Group("/columns/{column_id}", func() {
|
||||
m.Patch("", bind(api.EditProjectColumnOption{}), shared.EditProjectColumn)
|
||||
m.Delete("", shared.DeleteProjectColumn)
|
||||
m.Post("/default", shared.SetDefaultProjectColumn)
|
||||
m.Post("/issues/{issue_id}", shared.AddIssueToProjectColumn)
|
||||
m.Delete("/issues/{issue_id}", shared.RemoveIssueFromProjectColumn)
|
||||
})
|
||||
m.Post("/issues/{issue_id}/move", bind(api.MoveProjectIssueOption{}), shared.MoveProjectIssue)
|
||||
})
|
||||
}, writeChecks...)
|
||||
}
|
||||
|
||||
// mustEnableRepoProjects mirrors repo.MustEnableRepoProjects: the Projects unit can be
|
||||
// readable while repo-level boards are disallowed, and the web UI then hides them entirely.
|
||||
func mustEnableRepoProjects(ctx *context.APIContext) {
|
||||
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
|
||||
if !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
|
||||
func mustNotBeArchived(ctx *context.APIContext) {
|
||||
if ctx.Repo.Repository.IsArchived {
|
||||
@@ -1077,6 +1139,8 @@ func Routes() *web.Router {
|
||||
}
|
||||
|
||||
m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos)
|
||||
m.Get("/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqExploreSignIn(),
|
||||
reqProjectsUnitAccess(perm.AccessModeRead), shared.ListProjects)
|
||||
m.Group("/tokens", func() {
|
||||
m.Combo("").Get(user.ListAccessTokens).
|
||||
Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
|
||||
@@ -1112,6 +1176,9 @@ func Routes() *web.Router {
|
||||
m.Get("", user.GetUserSettings)
|
||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||
}, rejectPublicOnly())
|
||||
m.Group("/projects", func() {
|
||||
addProjectRoutes(m, reqToken())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
// Email addresses are always private account data.
|
||||
m.Combo("/emails", rejectPublicOnly()).
|
||||
Get(user.ListEmails).
|
||||
@@ -1690,6 +1757,9 @@ func Routes() *web.Router {
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||
})
|
||||
m.Group("/projects", func() {
|
||||
addProjectRoutes(m, reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived)
|
||||
}, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
|
||||
@@ -1753,6 +1823,9 @@ func Routes() *web.Router {
|
||||
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", org.SearchTeam)
|
||||
}, reqToken(), reqOrgMembership())
|
||||
m.Group("/projects", func() {
|
||||
addProjectRoutes(m, reqToken(), reqProjectsUnitAccess(perm.AccessModeWrite))
|
||||
}, reqProjectsUnitAccess(perm.AccessModeRead), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
m.Group("/labels", func() {
|
||||
m.Get("", org.ListLabels)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -237,6 +237,22 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
LockIssueOption api.LockIssueOption
|
||||
|
||||
// in:body
|
||||
CreateProjectOption api.CreateProjectOption
|
||||
// in:body
|
||||
EditProjectOption api.EditProjectOption
|
||||
|
||||
// in:body
|
||||
CreateProjectColumnOption api.CreateProjectColumnOption
|
||||
// in:body
|
||||
EditProjectColumnOption api.EditProjectColumnOption
|
||||
|
||||
// in:body
|
||||
MoveProjectColumnsOption api.MoveProjectColumnsOption
|
||||
|
||||
// in:body
|
||||
MoveProjectIssueOption api.MoveProjectIssueOption
|
||||
|
||||
// in:body
|
||||
MergeUpstreamRequest api.MergeUpstreamRequest
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package swagger
|
||||
|
||||
import (
|
||||
api "gitea.dev/modules/structs"
|
||||
)
|
||||
|
||||
// Project
|
||||
// swagger:response Project
|
||||
type swaggerResponseProject struct {
|
||||
// in:body
|
||||
Body api.Project `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectList
|
||||
// swagger:response ProjectList
|
||||
type swaggerResponseProjectList struct {
|
||||
// in:body
|
||||
Body []api.Project `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectColumn
|
||||
// swagger:response ProjectColumn
|
||||
type swaggerResponseProjectColumn struct {
|
||||
// in:body
|
||||
Body api.ProjectColumn `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectColumnList
|
||||
// swagger:response ProjectColumnList
|
||||
type swaggerResponseProjectColumnList struct {
|
||||
// in:body
|
||||
Body []api.ProjectColumn `json:"body"`
|
||||
}
|
||||
Reference in New Issue
Block a user