mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 22:13:26 +09: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:
co-authored by
Supen.Huang
Claude
Ember
Lunny Xiao
wxiaoguang
beardev-in
parent
8163139ec0
commit
7fae3d5db3
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user