feat: api for projects

This commit is contained in:
Dinesh Salunke 2023-11-18 18:32:22 +05:30
parent 6092b81563
commit b6f9d05180
7 changed files with 1396 additions and 58 deletions

View File

@ -95,6 +95,7 @@ type Project struct {
RepoID int64 `xorm:"INDEX"` RepoID int64 `xorm:"INDEX"`
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
CreatorID int64 `xorm:"NOT NULL"` CreatorID int64 `xorm:"NOT NULL"`
Creator *user_model.User `xorm:"-"`
IsClosed bool `xorm:"INDEX"` IsClosed bool `xorm:"INDEX"`
BoardType BoardType BoardType BoardType
CardType CardType CardType CardType
@ -115,6 +116,14 @@ func (p *Project) LoadOwner(ctx context.Context) (err error) {
return err return err
} }
func (p *Project) LoadCreator(ctx context.Context) (err error) {
if p.Creator != nil {
return nil
}
p.Creator, err = user_model.GetUserByID(ctx, p.CreatorID)
return err
}
func (p *Project) LoadRepo(ctx context.Context) (err error) { func (p *Project) LoadRepo(ctx context.Context) (err error) {
if p.RepoID == 0 || p.Repo != nil { if p.RepoID == 0 || p.Repo != nil {
return nil return nil
@ -348,7 +357,11 @@ func updateRepositoryProjectCount(ctx context.Context, repoID int64) error {
} }
// ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed // ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed
func ChangeProjectStatusByRepoIDAndID(ctx context.Context, repoID, projectID int64, isClosed bool) error { func ChangeProjectStatusByRepoIDAndID(
ctx context.Context,
repoID, projectID int64,
isClosed bool,
) error {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
@ -389,7 +402,11 @@ func ChangeProjectStatus(ctx context.Context, p *Project, isClosed bool) error {
func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error {
p.IsClosed = isClosed p.IsClosed = isClosed
p.ClosedDateUnix = timeutil.TimeStampNow() p.ClosedDateUnix = timeutil.TimeStampNow()
count, err := db.GetEngine(ctx).ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) count, err := db.GetEngine(ctx).
ID(p.ID).
Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).
Cols("is_closed", "closed_date_unix").
Update(p)
if err != nil { if err != nil {
return err return err
} }

View File

@ -88,6 +88,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/notify"
"code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/org"
"code.gitea.io/gitea/routers/api/v1/packages" "code.gitea.io/gitea/routers/api/v1/packages"
"code.gitea.io/gitea/routers/api/v1/projects"
"code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
@ -231,7 +232,11 @@ func repoAssignment() func(ctx *context.APIContext) {
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin") ctx.Error(
http.StatusForbidden,
"reqPackageAccess",
"user should have specific permission or be a site admin",
)
return return
} }
} }
@ -239,7 +244,9 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
// if a token is being used for auth, we check that it contains the required scope // if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods // if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { func tokenRequiresScopes(
requiredScopeCategories ...auth_model.AccessTokenScopeCategory,
) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
// no scope required // no scope required
if len(requiredScopeCategories) == 0 { if len(requiredScopeCategories) == 0 {
@ -257,27 +264,46 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// use the http method to determine the access level // use the http method to determine the access level
requiredScopeLevel := auth_model.Read requiredScopeLevel := auth_model.Read
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" ||
ctx.Req.Method == "DELETE" {
requiredScopeLevel = auth_model.Write requiredScopeLevel = auth_model.Write
} }
// get the required scope for the given access level and category // get the required scope for the given access level and category
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) requiredScopes := auth_model.GetRequiredScopes(
requiredScopeLevel,
requiredScopeCategories...)
// check if scope only applies to public resources // check if scope only applies to public resources
publicOnly, err := scope.PublicOnly() publicOnly, err := scope.PublicOnly()
if err != nil { if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) ctx.Error(
http.StatusForbidden,
"tokenRequiresScope",
"parsing public resource scope failed: "+err.Error(),
)
return return
} }
// this context is used by the middleware in the specific route // this context is used by the middleware in the specific route
ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository) ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly &&
ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization) auth_model.ContainsCategory(
requiredScopeCategories,
auth_model.AccessTokenScopeCategoryRepository,
)
ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly &&
auth_model.ContainsCategory(
requiredScopeCategories,
auth_model.AccessTokenScopeCategoryOrganization,
)
allow, err := scope.HasScope(requiredScopes...) allow, err := scope.HasScope(requiredScopes...)
if err != nil { if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) ctx.Error(
http.StatusForbidden,
"tokenRequiresScope",
"checking scope failed: "+err.Error(),
)
return return
} }
@ -285,7 +311,14 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
return return
} }
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) ctx.Error(
http.StatusForbidden,
"tokenRequiresScope",
fmt.Sprintf(
"token does not have at least one of required scope(s): %v",
requiredScopes,
),
)
} }
} }
@ -303,7 +336,11 @@ func reqToken() func(ctx *context.APIContext) {
if pubRepoExists && publicRepo.(bool) && if pubRepoExists && publicRepo.(bool) &&
ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") ctx.Error(
http.StatusForbidden,
"reqToken",
"token scope is limited to public repos",
)
return return
} }
@ -326,14 +363,19 @@ func reqToken() func(ctx *context.APIContext) {
func reqExploreSignIn() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if setting.Service.Explore.RequireSigninView && !ctx.IsSigned { if setting.Service.Explore.RequireSigninView && !ctx.IsSigned {
ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") ctx.Error(
http.StatusUnauthorized,
"reqExploreSignIn",
"you must be signed in to search for users",
)
} }
} }
} }
func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI &&
ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName {
return return
} }
if !ctx.IsBasicAuth { if !ctx.IsBasicAuth {
@ -367,7 +409,11 @@ func reqOwner() func(ctx *context.APIContext) {
func reqSelfOrAdmin() func(ctx *context.APIContext) { func reqSelfOrAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer { if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer {
ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") ctx.Error(
http.StatusForbidden,
"reqSelfOrAdmin",
"doer should be the site admin or be same as the contextUser",
)
return return
} }
} }
@ -377,7 +423,11 @@ func reqSelfOrAdmin() func(ctx *context.APIContext) {
func reqAdmin() func(ctx *context.APIContext) { func reqAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository") ctx.Error(
http.StatusForbidden,
"reqAdmin",
"user should be an owner or a collaborator with admin write of a repository",
)
return return
} }
} }
@ -387,7 +437,11 @@ func reqAdmin() func(ctx *context.APIContext) {
func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") ctx.Error(
http.StatusForbidden,
"reqRepoWriter",
"user should have a permission to write to a repo",
)
return return
} }
} }
@ -396,8 +450,13 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin // reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin
func reqRepoBranchWriter(ctx *context.APIContext) { func reqRepoBranchWriter(ctx *context.APIContext) {
options, ok := web.GetForm(ctx).(api.FileOptionInterface) options, ok := web.GetForm(ctx).(api.FileOptionInterface)
if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { if !ok ||
ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch") (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) {
ctx.Error(
http.StatusForbidden,
"reqRepoBranchWriter",
"user should have a permission to write to this branch",
)
return return
} }
} }
@ -406,7 +465,11 @@ func reqRepoBranchWriter(ctx *context.APIContext) {
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") ctx.Error(
http.StatusForbidden,
"reqRepoReader",
"user should have specific read permission or be a repo admin or a site admin",
)
return return
} }
} }
@ -416,7 +479,11 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
func reqAnyRepoReader() func(ctx *context.APIContext) { func reqAnyRepoReader() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() { if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") ctx.Error(
http.StatusForbidden,
"reqAnyRepoReader",
"user should have any permission to read repository or permissions of site admin",
)
return return
} }
} }
@ -671,7 +738,11 @@ func mustEnableWiki(ctx *context.APIContext) {
func mustNotBeArchived(ctx *context.APIContext) { func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived { if ctx.Repo.Repository.IsArchived {
ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) ctx.Error(
http.StatusLocked,
"RepoArchived",
fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString()),
)
return return
} }
} }
@ -689,7 +760,11 @@ func bind[T any](_ T) any {
theObj := new(T) // create a new form obj for every request but not use obj directly theObj := new(T) // create a new form obj for every request but not use obj directly
errs := binding.Bind(ctx.Req, theObj) errs := binding.Bind(ctx.Req, theObj)
if len(errs) > 0 { if len(errs) > 0 {
ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error())) ctx.Error(
http.StatusUnprocessableEntity,
"validationError",
fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()),
)
return return
} }
web.SetForm(ctx, theObj) web.SetForm(ctx, theObj)
@ -739,7 +814,11 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
return return
} }
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) log.Info(
"Failed authentication attempt for %s from %s",
ctx.Doer.Name,
ctx.RemoteAddr(),
)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.JSON(http.StatusForbidden, map[string]string{ ctx.JSON(http.StatusForbidden, map[string]string{
"message": "This account is prohibited from signing in, please contact your site administrator.", "message": "This account is prohibited from signing in, please contact your site administrator.",
@ -800,7 +879,9 @@ func Routes() *web.Route {
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option // setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
AllowedMethods: setting.CORSConfig.Methods, AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials, AllowCredentials: setting.CORSConfig.AllowCredentials,
AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...), AllowedHeaders: append(
[]string{"Authorization", "X-Gitea-OTP"},
setting.CORSConfig.Headers...),
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
})) }))
} }
@ -880,7 +961,12 @@ func Routes() *web.Route {
m.Get("/heatmap", user.GetUserHeatmapData) m.Get("/heatmap", user.GetUserHeatmapData)
} }
m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos) m.Get(
"/repos",
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository),
reqExploreSignIn(),
user.ListUserRepos,
)
m.Group("/tokens", func() { m.Group("/tokens", func() {
m.Combo("").Get(user.ListAccessTokens). m.Combo("").Get(user.ListAccessTokens).
Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
@ -968,7 +1054,8 @@ func Routes() *web.Route {
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey) m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope) // (repo scope)
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos). m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).
Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create) Post(bind(api.CreateRepoOption{}), repo.Create)
// (repo scope) // (repo scope)
@ -996,17 +1083,27 @@ func Routes() *web.Route {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar) m.Delete("", user.DeleteAvatar)
}, reqToken()) }, reqToken())
m.Combo("/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)).
Get(projects.ListUserProjects).
Post(bind(api.NewProjectPayload{}), projects.CreateUserProject)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope) // Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos", m.Post(
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), "/org/{org}/repos",
tokenRequiresScopes(
auth_model.AccessTokenScopeCategoryOrganization,
auth_model.AccessTokenScopeCategoryRepository,
),
reqToken(), reqToken(),
bind(api.CreateRepoOption{}), bind(api.CreateRepoOption{}),
repo.CreateOrgRepoDeprecated) repo.CreateOrgRepoDeprecated,
)
// requires repo scope // requires repo scope
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID) m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).
Get(repo.GetByID)
// Repos (requires repo scope) // Repos (requires repo scope)
m.Group("/repos", func() { m.Group("/repos", func() {
@ -1019,7 +1116,13 @@ func Routes() *web.Route {
m.Combo("").Get(reqAnyRepoReader(), repo.Get). m.Combo("").Get(reqAnyRepoReader(), repo.Get).
Delete(reqToken(), reqOwner(), repo.Delete). Delete(reqToken(), reqOwner(), repo.Delete).
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate) m.Post(
"/generate",
reqToken(),
reqRepoReader(unit.TypeCode),
bind(api.GenerateRepoOption{}),
repo.Generate,
)
m.Group("/transfer", func() { m.Group("/transfer", func() {
m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
m.Post("/accept", repo.AcceptTransfer) m.Post("/accept", repo.AcceptTransfer)
@ -1045,7 +1148,12 @@ func Routes() *web.Route {
m.Combo("").Get(repo.GetHook). m.Combo("").Get(repo.GetHook).
Patch(bind(api.EditHookOption{}), repo.EditHook). Patch(bind(api.EditHookOption{}), repo.EditHook).
Delete(repo.DeleteHook) Delete(repo.DeleteHook)
m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook) m.Post(
"/tests",
context.ReferencesGitRepo(),
context.RepoRefForAPI,
repo.TestHook,
)
}) })
}, reqToken(), reqAdmin(), reqWebhooksEnabled()) }, reqToken(), reqAdmin(), reqWebhooksEnabled())
m.Group("/collaborators", func() { m.Group("/collaborators", func() {
@ -1065,31 +1173,79 @@ func Routes() *web.Route {
Put(reqAdmin(), repo.AddTeam). Put(reqAdmin(), repo.AddTeam).
Delete(reqAdmin(), repo.DeleteTeam) Delete(reqAdmin(), repo.DeleteTeam)
}, reqToken()) }, reqToken())
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) m.Get(
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) "/raw/*",
context.ReferencesGitRepo(),
context.RepoRefForAPI,
reqRepoReader(unit.TypeCode),
repo.GetRawFile,
)
m.Get(
"/media/*",
context.ReferencesGitRepo(),
context.RepoRefForAPI,
reqRepoReader(unit.TypeCode),
repo.GetRawFileOrLFS,
)
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks). m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Group("/branches", func() { m.Group("/branches", func() {
m.Get("", repo.ListBranches) m.Get("", repo.ListBranches)
m.Get("/*", repo.GetBranch) m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Delete(
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) "/*",
reqToken(),
reqRepoWriter(unit.TypeCode),
mustNotBeArchived,
repo.DeleteBranch,
)
m.Post(
"",
reqToken(),
reqRepoWriter(unit.TypeCode),
mustNotBeArchived,
bind(api.CreateBranchRepoOption{}),
repo.CreateBranch,
)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() { m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections) m.Get("", repo.ListBranchProtections)
m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection) m.Post(
"",
bind(api.CreateBranchProtectionOption{}),
mustNotBeArchived,
repo.CreateBranchProtection,
)
m.Group("/{name}", func() { m.Group("/{name}", func() {
m.Get("", repo.GetBranchProtection) m.Get("", repo.GetBranchProtection)
m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) m.Patch(
"",
bind(api.EditBranchProtectionOption{}),
mustNotBeArchived,
repo.EditBranchProtection,
)
m.Delete("", repo.DeleteBranchProtection) m.Delete("", repo.DeleteBranchProtection)
}) })
}, reqToken(), reqAdmin()) }, reqToken(), reqAdmin())
m.Group("/tags", func() { m.Group("/tags", func() {
m.Get("", repo.ListTags) m.Get("", repo.ListTags)
m.Get("/*", repo.GetTag) m.Get("/*", repo.GetTag)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Post(
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) "",
reqToken(),
reqRepoWriter(unit.TypeCode),
mustNotBeArchived,
bind(api.CreateTagOption{}),
repo.CreateTag,
)
m.Delete(
"/*",
reqToken(),
reqRepoWriter(unit.TypeCode),
mustNotBeArchived,
repo.DeleteTag,
)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/keys", func() { m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys). m.Combo("").Get(repo.ListDeployKeys).
@ -1107,7 +1263,14 @@ func Routes() *web.Route {
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage) Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
m.Get("/revisions/{pageName}", repo.ListPageRevisions) m.Get("/revisions/{pageName}", repo.ListPageRevisions)
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) m.Post(
"/new",
reqToken(),
mustNotBeArchived,
reqRepoWriter(unit.TypeWiki),
bind(api.CreateWikiPageOptions{}),
repo.NewWikiPage,
)
m.Get("/pages", repo.ListWikiPages) m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki) }, mustEnableWiki)
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
@ -1152,7 +1315,13 @@ func Routes() *web.Route {
Get(repo.GetPushMirrorByName) Get(repo.GetPushMirrorByName)
}, reqAdmin(), reqToken()) }, reqAdmin(), reqToken())
m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig) m.Get(
"/editorconfig/{filename}",
context.ReferencesGitRepo(),
context.RepoRefForAPI,
reqRepoReader(unit.TypeCode),
repo.GetEditorconfig,
)
m.Group("/pulls", func() { m.Group("/pulls", func() {
m.Combo("").Get(repo.ListPullRequests). m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest) Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
@ -1178,7 +1347,12 @@ func Routes() *web.Route {
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments"). m.Combo("/comments").
Get(repo.GetPullReviewComments) Get(repo.GetPullReviewComments)
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post(
"/dismissals",
reqToken(),
bind(api.DismissPullReviewOptions{}),
repo.DismissPullReview,
)
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
}) })
}) })
@ -1210,15 +1384,47 @@ func Routes() *web.Route {
m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote) m.Get("/notes/{sha}", repo.GetNote)
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) m.Post(
"/diffpatch",
reqRepoWriter(unit.TypeCode),
reqToken(),
bind(api.ApplyDiffPatchFileOptions{}),
mustNotBeArchived,
repo.ApplyDiffPatch,
)
m.Group("/contents", func() { m.Group("/contents", func() {
m.Get("", repo.GetContentsList) m.Get("", repo.GetContentsList)
m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) m.Post(
"",
reqToken(),
bind(api.ChangeFilesOptions{}),
reqRepoBranchWriter,
mustNotBeArchived,
repo.ChangeFiles,
)
m.Get("/*", repo.GetContents) m.Get("/*", repo.GetContents)
m.Group("/*", func() { m.Group("/*", func() {
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) m.Post(
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) "",
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) bind(api.CreateFileOptions{}),
reqRepoBranchWriter,
mustNotBeArchived,
repo.CreateFile,
)
m.Put(
"",
bind(api.UpdateFileOptions{}),
reqRepoBranchWriter,
mustNotBeArchived,
repo.UpdateFile,
)
m.Delete(
"",
bind(api.DeleteFileOptions{}),
reqRepoBranchWriter,
mustNotBeArchived,
repo.DeleteFile,
)
}, reqToken()) }, reqToken())
}, reqRepoReader(unit.TypeCode)) }, reqRepoReader(unit.TypeCode))
m.Get("/signing-key.gpg", misc.SigningKey) m.Get("/signing-key.gpg", misc.SigningKey)
@ -1232,7 +1438,11 @@ func Routes() *web.Route {
}, reqAnyRepoReader()) }, reqAnyRepoReader())
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) m.Get(
"/issue_config/validate",
context.ReferencesGitRepo(),
repo.ValidateIssueConfig,
)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds) m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed) m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
@ -1290,7 +1500,8 @@ func Routes() *web.Route {
m.Group("/comments", func() { m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments). m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). m.Combo("/{id}", reqToken()).
Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated) Delete(repo.DeleteIssueCommentDeprecated)
}) })
m.Get("/timeline", repo.ListIssueCommentsAndTimeline) m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
@ -1308,7 +1519,8 @@ func Routes() *web.Route {
Delete(repo.ResetIssueTime) Delete(repo.ResetIssueTime)
m.Delete("/{id}", repo.DeleteTime) m.Delete("/{id}", repo.DeleteTime)
}, reqToken()) }, reqToken())
m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Combo("/deadline").
Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() { m.Group("/stopwatch", func() {
m.Post("/start", repo.StartIssueStopwatch) m.Post("/start", repo.StartIssueStopwatch)
m.Post("/stop", repo.StopIssueStopwatch) m.Post("/stop", repo.StopIssueStopwatch)
@ -1363,6 +1575,12 @@ func Routes() *web.Route {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
}) })
m.Group("/projects", func() {
m.
Combo("").
Get(projects.ListRepoProjects).
Post(bind(api.NewProjectPayload{}), projects.CreateRepoProject)
}, mustEnableIssues)
}, repoAssignment()) }, repoAssignment())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
@ -1370,20 +1588,43 @@ func Routes() *web.Route {
m.Group("/packages/{username}", func() { m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}/{version}", func() { m.Group("/{type}/{name}/{version}", func() {
m.Get("", reqToken(), packages.GetPackage) m.Get("", reqToken(), packages.GetPackage)
m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Delete(
"",
reqToken(),
reqPackageAccess(perm.AccessModeWrite),
packages.DeletePackage,
)
m.Get("/files", reqToken(), packages.ListPackageFiles) m.Get("/files", reqToken(), packages.ListPackageFiles)
}) })
m.Get("/", reqToken(), packages.ListPackages) m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
// Organizations // Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) m.Get(
"/user/orgs",
reqToken(),
tokenRequiresScopes(
auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization,
),
org.ListMyOrgs,
)
m.Group("/users/{username}/orgs", func() { m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs) m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) m.Post(
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) "/orgs",
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization),
reqToken(),
bind(api.CreateOrgOption{}),
org.Create,
)
m.Get(
"/orgs",
org.GetAll,
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization),
)
m.Group("/orgs/{org}", func() { m.Group("/orgs/{org}", func() {
m.Combo("").Get(org.Get). m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
@ -1414,7 +1655,13 @@ func Routes() *web.Route {
}, reqToken(), reqOrgMembership()) }, reqToken(), reqOrgMembership())
m.Group("/labels", func() { m.Group("/labels", func() {
m.Get("", org.ListLabels) m.Get("", org.ListLabels)
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel) m.Post(
"",
reqToken(),
reqOrgOwnership(),
bind(api.CreateLabelOption{}),
org.CreateLabel,
)
m.Combo("/{id}").Get(reqToken(), org.GetLabel). m.Combo("/{id}").Get(reqToken(), org.GetLabel).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel). Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel) Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
@ -1431,6 +1678,12 @@ func Routes() *web.Route {
m.Delete("", org.DeleteAvatar) m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership()) }, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds) m.Get("/activities/feeds", org.ListOrgActivityFeeds)
m.Group("/projects", func() {
m.Combo("").
Get(projects.ListOrgProjects).
Post(bind(api.NewProjectPayload{}), projects.CreateOrgProject)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken(), reqOrgMembership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() { m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam). m.Combo("").Get(reqToken(), org.GetTeam).
@ -1493,6 +1746,13 @@ func Routes() *web.Route {
}) })
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
m.Group("/projects", func() {
m.
Combo("/{id}").
Get(projects.GetProject).
Patch(bind(api.UpdateProjectPayload{}), projects.UpdateProject).
Delete(projects.DeleteProject)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken())
m.Group("/topics", func() { m.Group("/topics", func() {
m.Get("/search", repo.TopicSearch) m.Get("/search", repo.TopicSearch)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

View File

@ -0,0 +1,407 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package projects
import (
"net/http"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
)
func innerCreateProject(
ctx *context.APIContext,
project_type project_model.Type,
) {
form := web.GetForm(ctx).(*api.NewProjectPayload)
project := &project_model.Project{
RepoID: 0,
OwnerID: ctx.Doer.ID,
Title: form.Title,
Description: form.Description,
CreatorID: ctx.Doer.ID,
BoardType: project_model.BoardType(form.BoardType),
Type: project_type,
}
if ctx.ContextUser != nil {
project.OwnerID = ctx.ContextUser.ID
}
if project_type == project_model.TypeRepository {
project.RepoID = ctx.Repo.Repository.ID
}
if err := project_model.NewProject(ctx, project); err != nil {
ctx.Error(http.StatusInternalServerError, "NewProject", err)
return
}
project, err := project_model.GetProjectByID(ctx, project.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "NewProject", err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIProject(ctx, project))
}
func CreateUserProject(ctx *context.APIContext) {
// swagger:operation POST /user/projects project projectCreateUserProject
// ---
// summary: Create a user project
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: project
// in: body
// required: true
// schema: { "$ref": "#/definitions/NewProjectPayload" }
// responses:
// "201":
// "$ref": "#/responses/Project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
innerCreateProject(ctx, project_model.TypeIndividual)
}
func CreateOrgProject(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/projects project projectCreateOrgProject
// ---
// summary: Create a organization project
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: org
// in: path
// description: owner of repo
// type: string
// required: true
// - name: project
// in: body
// required: true
// schema: { "$ref": "#/definitions/NewProjectPayload" }
// responses:
// "201":
// "$ref": "#/responses/Project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
innerCreateProject(ctx, project_model.TypeOrganization)
}
func CreateRepoProject(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects project projectCreateRepositoryProject
// ---
// summary: Create a repository project
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of repo
// type: string
// required: true
// - name: repo
// in: path
// description: repo
// type: string
// required: true
// - name: project
// in: body
// required: true
// schema: { "$ref": "#/definitions/NewProjectPayload" }
// responses:
// "201":
// "$ref": "#/responses/Project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
innerCreateProject(ctx, project_model.TypeRepository)
}
func GetProject(ctx *context.APIContext) {
// swagger:operation GET /projects/{id} project projectGetProject
// ---
// summary: Get project
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetProjectByID", err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project))
}
func UpdateProject(ctx *context.APIContext) {
// swagger:operation PATCH /projects/{id} project projectUpdateProject
// ---
// summary: Update project
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: string
// required: true
// - name: project
// in: body
// required: true
// schema: { "$ref": "#/definitions/UpdateProjectPayload" }
// responses:
// "200":
// "$ref": "#/responses/Project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.UpdateProjectPayload)
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "UpdateProject", err)
}
return
}
if project.Title != form.Title {
project.Title = form.Title
}
if project.Description != form.Description {
project.Description = form.Description
}
err = project_model.UpdateProject(ctx, project)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProject", err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project))
}
func DeleteProject(ctx *context.APIContext) {
// swagger:operation DELETE /projects/{id} project projectDeleteProject
// ---
// summary: Delete project
// parameters:
// - name: id
// in: path
// description: id of the project
// type: string
// required: true
// responses:
// "204":
// "description": "Deleted the project"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if err := project_model.DeleteProjectByID(ctx, ctx.ParamsInt64(":id")); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteProjectByID", err)
return
}
ctx.Status(http.StatusNoContent)
}
func ListUserProjects(ctx *context.APIContext) {
// swagger:operation GET /user/projects project projectListUserProjects
// ---
// summary: List repository projects
// produces:
// - application/json
// parameters:
// - name: closed
// in: query
// description: include closed issues or not
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
OwnerID: ctx.Doer.ID,
Page: ctx.FormInt("page"),
IsClosed: ctx.FormOptionalBool("closed"),
Type: project_model.TypeIndividual,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}
func ListOrgProjects(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/projects project projectListOrgProjects
// ---
// summary: List repository projects
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: owner of the repository
// type: string
// required: true
// - name: closed
// in: query
// description: include closed issues or not
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
OwnerID: ctx.Org.Organization.AsUser().ID,
Page: ctx.FormInt("page"),
IsClosed: ctx.FormOptionalBool("closed"),
Type: project_model.TypeOrganization,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}
func ListRepoProjects(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects project projectListRepositoryProjects
// ---
// summary: List repository projects
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repository
// type: string
// required: true
// - name: repo
// in: path
// description: repo
// type: string
// required: true
// - name: closed
// in: query
// description: include closed issues or not
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{
RepoID: ctx.Repo.Repository.ID,
Page: ctx.FormInt("page"),
IsClosed: ctx.FormOptionalBool("closed"),
Type: project_model.TypeRepository,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Projects", err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}

View File

@ -0,0 +1,22 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "code.gitea.io/gitea/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"`
}

View File

@ -0,0 +1,66 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
project_model "code.gitea.io/gitea/models/project"
api "code.gitea.io/gitea/modules/structs"
)
func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Project {
apiProject := &api.Project{
Title: project.Title,
Description: project.Description,
BoardType: uint8(project.BoardType),
IsClosed: project.IsClosed,
Created: project.CreatedUnix.AsTime(),
Updated: project.UpdatedUnix.AsTime(),
Closed: project.ClosedDateUnix.AsTime(),
}
// try to laod the repo
project.LoadRepo(ctx)
if project.Repo != nil {
apiProject.Repo = &api.RepositoryMeta{
ID: project.RepoID,
Name: project.Repo.Name,
Owner: project.Repo.OwnerName,
FullName: project.Repo.FullName(),
}
}
project.LoadCreator(ctx)
if project.Creator != nil {
apiProject.Creator = &api.User{
ID: project.Creator.ID,
UserName: project.Creator.Name,
FullName: project.Creator.FullName,
}
}
project.LoadOwner(ctx)
if project.Owner != nil {
apiProject.Owner = &api.User{
ID: project.Owner.ID,
UserName: project.Owner.Name,
FullName: project.Owner.FullName,
}
}
return apiProject
}
func ToAPIProjectList(
ctx context.Context,
projects []*project_model.Project,
) ([]*api.Project, error) {
result := make([]*api.Project, len(projects))
for i := range projects {
result[i] = ToAPIProject(ctx, projects[i])
}
return result, nil
}

View File

@ -2311,6 +2311,75 @@
} }
} }
}, },
"/orgs/{org}/projects": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "List repository projects",
"operationId": "projectListOrgProjects",
"parameters": [
{
"type": "string",
"description": "owner of the repository",
"name": "org",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "include closed issues or not",
"name": "closed",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
]
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "Create a organization project",
"operationId": "projectCreateOrgProject",
"parameters": [
{
"type": "string",
"description": "owner of repo",
"name": "org",
"in": "path",
"required": true
},
{
"name": "project",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/NewProjectPayload"
}
}
]
}
},
"/orgs/{org}/public_members": { "/orgs/{org}/public_members": {
"get": { "get": {
"produces": [ "produces": [
@ -2912,6 +2981,73 @@
} }
} }
}, },
"/projects/{id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "Get project",
"operationId": "projectGetProject",
"parameters": [
{
"type": "string",
"description": "id of the project",
"name": "id",
"in": "path",
"required": true
}
]
},
"delete": {
"tags": [
"project"
],
"summary": "Delete project",
"operationId": "projectDeleteProject",
"parameters": [
{
"type": "string",
"description": "id of the project",
"name": "id",
"in": "path",
"required": true
}
]
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "Update project",
"operationId": "projectUpdateProject",
"parameters": [
{
"type": "string",
"description": "id of the project",
"name": "id",
"in": "path",
"required": true
},
{
"name": "project",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UpdateProjectPayload"
}
}
]
}
},
"/repos/issues/search": { "/repos/issues/search": {
"get": { "get": {
"produces": [ "produces": [
@ -10140,6 +10276,89 @@
} }
} }
}, },
"/repos/{owner}/{repo}/projects": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "List repository projects",
"operationId": "projectListRepositoryProjects",
"parameters": [
{
"type": "string",
"description": "owner of the repository",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "include closed issues or not",
"name": "closed",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
]
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "Create a repository project",
"operationId": "projectCreateRepositoryProject",
"parameters": [
{
"type": "string",
"description": "owner of repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "repo",
"name": "repo",
"in": "path",
"required": true
},
{
"name": "project",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/NewProjectPayload"
}
}
]
}
},
"/repos/{owner}/{repo}/pulls": { "/repos/{owner}/{repo}/pulls": {
"get": { "get": {
"produces": [ "produces": [
@ -15481,6 +15700,61 @@
} }
} }
}, },
"/user/projects": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "List repository projects",
"operationId": "projectListUserProjects",
"parameters": [
{
"type": "boolean",
"description": "include closed issues or not",
"name": "closed",
"in": "query"
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
]
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "Create a user project",
"operationId": "projectCreateUserProject",
"parameters": [
{
"name": "project",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/NewProjectPayload"
}
}
]
}
},
"/user/repos": { "/user/repos": {
"get": { "get": {
"produces": [ "produces": [
@ -21310,6 +21584,55 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"Project": {
"type": "object",
"properties": {
"board_type": {
"type": "integer",
"format": "uint8",
"x-go-name": "BoardType"
},
"closed_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Closed"
},
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"creator": {
"$ref": "#/definitions/User"
},
"description": {
"type": "string",
"x-go-name": "Description"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"is_closed": {
"type": "boolean",
"x-go-name": "IsClosed"
},
"repository": {
"$ref": "#/definitions/RepositoryMeta"
},
"title": {
"type": "string",
"x-go-name": "Title"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"PublicKey": { "PublicKey": {
"description": "PublicKey publickey is a user key to push code to repository", "description": "PublicKey publickey is a user key to push code to repository",
"type": "object", "type": "object",
@ -23628,6 +23951,21 @@
} }
} }
}, },
"Project": {
"description": "Project",
"schema": {
"$ref": "#/definitions/Project"
}
},
"ProjectList": {
"description": "ProjectList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Project"
}
}
},
"PublicKey": { "PublicKey": {
"description": "PublicKey", "description": "PublicKey",
"schema": { "schema": {

View File

@ -0,0 +1,228 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/unittest"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPICreateUserProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban)
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeWriteIssue,
auth_model.AccessTokenScopeWriteUser,
)
urlStr := fmt.Sprintf("/api/v1/user/projects?token=%s", token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{
Title: title,
Description: description,
BoardType: board_type,
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiProject api.Project
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, title, apiProject.Title)
assert.Equal(t, description, apiProject.Description)
assert.Equal(t, board_type, apiProject.BoardType)
assert.Equal(t, "user2", apiProject.Creator.UserName)
}
func TestAPICreateOrgProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban)
orgName := "org17"
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeWriteIssue,
auth_model.AccessTokenScopeWriteOrganization,
)
urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects?token=%s", orgName, token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{
Title: title,
Description: description,
BoardType: board_type,
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiProject api.Project
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, title, apiProject.Title)
assert.Equal(t, description, apiProject.Description)
assert.Equal(t, board_type, apiProject.BoardType)
assert.Equal(t, "org17", apiProject.Creator.UserName)
}
func TestAPICreateRepoProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban)
ownerName := "user2"
repoName := "repo1"
token := getUserToken(
t,
ownerName,
auth_model.AccessTokenScopeWriteIssue,
auth_model.AccessTokenScopeWriteOrganization,
)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects?token=%s", ownerName, repoName, token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{
Title: title,
Description: description,
BoardType: board_type,
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiProject api.Project
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, title, apiProject.Title)
assert.Equal(t, description, apiProject.Description)
assert.Equal(t, board_type, apiProject.BoardType)
assert.Equal(t, "repo1", apiProject.Repo.Name)
}
func TestAPIListUserProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/user/projects"))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequest(t, "GET", link.String())
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIListOrgProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
orgName := "org17"
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeReadOrganization,
auth_model.AccessTokenScopeReadIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", orgName))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequest(t, "GET", link.String())
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIListRepoProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ownerName := "user2"
repoName := "repo1"
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeReadRepository,
auth_model.AccessTokenScopeReadIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequest(t, "GET", link.String())
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIGetProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequest(t, "GET", link.String())
var apiProject *api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, "First project", apiProject.Title)
assert.Equal(t, "repo1", apiProject.Repo.Name)
assert.Equal(t, "user2", apiProject.Creator.UserName)
}
func TestAPIUpdateProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{
Title: "First project updated",
})
var apiProject *api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, "First project updated", apiProject.Title)
}
func TestAPIDeleteProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(
t,
"user2",
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteIssue,
)
link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1))
link.RawQuery = url.Values{"token": {token}}.Encode()
req := NewRequest(t, "DELETE", link.String())
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &project_model.Project{ID: 1})
}