Add user webhooks (#21563)
Currently we can add webhooks for organizations but not for users. This PR adds the latter. You can access it from the current users settings. 
This commit is contained in:
		
							parent
							
								
									dad057b639
								
							
						
					
					
						commit
						2173f14708
					
				|  | @ -60,6 +60,7 @@ Gitea supports the following scopes for tokens: | |||
| |     **write:public_key** | Grant read/write access to public keys | | ||||
| |     **read:public_key** | Grant read-only access to public keys | | ||||
| | **admin:org_hook** | Grants full access to organizational-level hooks | | ||||
| | **admin:user_hook** | Grants full access to user-level hooks | | ||||
| | **notification** | Grants full access to notifications | | ||||
| | **user** | Grants full access to user profile info | | ||||
| |     **read:user** | Grants read access to user's profile | | ||||
|  |  | |||
|  | @ -32,6 +32,8 @@ const ( | |||
| 
 | ||||
| 	AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook" | ||||
| 
 | ||||
| 	AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook" | ||||
| 
 | ||||
| 	AccessTokenScopeNotification AccessTokenScope = "notification" | ||||
| 
 | ||||
| 	AccessTokenScopeUser       AccessTokenScope = "user" | ||||
|  | @ -64,7 +66,7 @@ type AccessTokenScopeBitmap uint64 | |||
| const ( | ||||
| 	// AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`.
 | ||||
| 	AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits | | ||||
| 		AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | | ||||
| 		AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits | | ||||
| 		AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits | | ||||
| 		AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits | ||||
| 
 | ||||
|  | @ -86,6 +88,8 @@ const ( | |||
| 
 | ||||
| 	AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota | ||||
| 
 | ||||
| 	AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota | ||||
| 
 | ||||
| 	AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota | ||||
| 
 | ||||
| 	AccessTokenScopeUserBits       AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits | ||||
|  | @ -123,6 +127,7 @@ var allAccessTokenScopes = []AccessTokenScope{ | |||
| 	AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey, | ||||
| 	AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook, | ||||
| 	AccessTokenScopeAdminOrgHook, | ||||
| 	AccessTokenScopeAdminUserHook, | ||||
| 	AccessTokenScopeNotification, | ||||
| 	AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow, | ||||
| 	AccessTokenScopeDeleteRepo, | ||||
|  | @ -147,6 +152,7 @@ var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{ | |||
| 	AccessTokenScopeWriteRepoHook:    AccessTokenScopeWriteRepoHookBits, | ||||
| 	AccessTokenScopeReadRepoHook:     AccessTokenScopeReadRepoHookBits, | ||||
| 	AccessTokenScopeAdminOrgHook:     AccessTokenScopeAdminOrgHookBits, | ||||
| 	AccessTokenScopeAdminUserHook:    AccessTokenScopeAdminUserHookBits, | ||||
| 	AccessTokenScopeNotification:     AccessTokenScopeNotificationBits, | ||||
| 	AccessTokenScopeUser:             AccessTokenScopeUserBits, | ||||
| 	AccessTokenScopeReadUser:         AccessTokenScopeReadUserBits, | ||||
|  | @ -263,7 +269,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope { | |||
| 	scope := AccessTokenScope(strings.Join(scopes, ",")) | ||||
| 	scope = AccessTokenScope(strings.ReplaceAll( | ||||
| 		string(scope), | ||||
| 		"repo,admin:org,admin:public_key,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", | ||||
| 		"repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", | ||||
| 		"all", | ||||
| 	)) | ||||
| 	return scope | ||||
|  |  | |||
|  | @ -40,8 +40,8 @@ func TestAccessTokenScope_Normalize(t *testing.T) { | |||
| 		{"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil}, | ||||
| 		{"admin:application,write:application,user", "user,admin:application", nil}, | ||||
| 		{"all", "all", nil}, | ||||
| 		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil}, | ||||
| 		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil}, | ||||
| 		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil}, | ||||
| 		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, test := range tests { | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ | |||
| 
 | ||||
| - | ||||
|   id: 3 | ||||
|   org_id: 3 | ||||
|   owner_id: 3 | ||||
|   repo_id: 3 | ||||
|   url: www.example.com/url3 | ||||
|   content_type: 1 # json | ||||
|  |  | |||
|  | @ -467,6 +467,8 @@ var migrations = []Migration{ | |||
| 
 | ||||
| 	// v244 -> v245
 | ||||
| 	NewMigration("Add NeedApproval to actions tables", v1_20.AddNeedApprovalToActionRun), | ||||
| 	// v245 -> v246
 | ||||
| 	NewMigration("Rename Webhook org_id to owner_id", v1_20.RenameWebhookOrgToOwner), | ||||
| } | ||||
| 
 | ||||
| // GetCurrentDBVersion returns the current db version
 | ||||
|  |  | |||
|  | @ -0,0 +1,74 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | ||||
| // SPDX-License-Identifier: MIT
 | ||||
| 
 | ||||
| package v1_20 //nolint
 | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/migrations/base" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
| 
 | ||||
| func RenameWebhookOrgToOwner(x *xorm.Engine) error { | ||||
| 	type Webhook struct { | ||||
| 		OrgID int64 `xorm:"INDEX"` | ||||
| 	} | ||||
| 
 | ||||
| 	// This migration maybe rerun so that we should check if it has been run
 | ||||
| 	ownerExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "owner_id") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if ownerExist { | ||||
| 		orgExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "org_id") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if !orgExist { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 	if err := sess.Begin(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := sess.Sync2(new(Webhook)); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if ownerExist { | ||||
| 		if err := base.DropTableColumns(sess, "webhook", "owner_id"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	switch { | ||||
| 	case setting.Database.Type.IsMySQL(): | ||||
| 		inferredTable, err := x.TableInfo(new(Webhook)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id")) | ||||
| 		if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	case setting.Database.Type.IsMSSQL(): | ||||
| 		if _, err := sess.Exec("sp_rename 'webhook.org_id', 'owner_id', 'COLUMN'"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	default: | ||||
| 		if _, err := sess.Exec("ALTER TABLE `webhook` RENAME COLUMN org_id TO owner_id"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sess.Commit() | ||||
| } | ||||
|  | @ -122,7 +122,7 @@ func IsValidHookContentType(name string) bool { | |||
| type Webhook struct { | ||||
| 	ID                        int64 `xorm:"pk autoincr"` | ||||
| 	RepoID                    int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook
 | ||||
| 	OrgID                     int64 `xorm:"INDEX"` | ||||
| 	OwnerID                   int64 `xorm:"INDEX"` | ||||
| 	IsSystemWebhook           bool | ||||
| 	URL                       string `xorm:"url TEXT"` | ||||
| 	HTTPMethod                string `xorm:"http_method"` | ||||
|  | @ -412,11 +412,11 @@ func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) { | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // GetWebhookByOrgID returns webhook of organization by given ID.
 | ||||
| func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { | ||||
| // GetWebhookByOwnerID returns webhook of a user or organization by given ID.
 | ||||
| func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) { | ||||
| 	return getWebhook(&Webhook{ | ||||
| 		ID:      id, | ||||
| 		OrgID: orgID, | ||||
| 		OwnerID: ownerID, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
|  | @ -424,7 +424,7 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { | |||
| type ListWebhookOptions struct { | ||||
| 	db.ListOptions | ||||
| 	RepoID   int64 | ||||
| 	OrgID    int64 | ||||
| 	OwnerID  int64 | ||||
| 	IsActive util.OptionalBool | ||||
| } | ||||
| 
 | ||||
|  | @ -433,8 +433,8 @@ func (opts *ListWebhookOptions) toCond() builder.Cond { | |||
| 	if opts.RepoID != 0 { | ||||
| 		cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) | ||||
| 	} | ||||
| 	if opts.OrgID != 0 { | ||||
| 		cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID}) | ||||
| 	if opts.OwnerID != 0 { | ||||
| 		cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) | ||||
| 	} | ||||
| 	if !opts.IsActive.IsNone() { | ||||
| 		cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) | ||||
|  | @ -503,10 +503,10 @@ func DeleteWebhookByRepoID(repoID, id int64) error { | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // DeleteWebhookByOrgID deletes webhook of organization by given ID.
 | ||||
| func DeleteWebhookByOrgID(orgID, id int64) error { | ||||
| // DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
 | ||||
| func DeleteWebhookByOwnerID(ownerID, id int64) error { | ||||
| 	return deleteWebhook(&Webhook{ | ||||
| 		ID:      id, | ||||
| 		OrgID: orgID, | ||||
| 		OwnerID: ownerID, | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ import ( | |||
| func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { | ||||
| 	webhooks := make([]*Webhook, 0, 5) | ||||
| 	return webhooks, db.GetEngine(ctx). | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false). | ||||
| 		Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false). | ||||
| 		Find(&webhooks) | ||||
| } | ||||
| 
 | ||||
|  | @ -23,7 +23,7 @@ func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { | |||
| func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) { | ||||
| 	webhook := &Webhook{ID: id} | ||||
| 	has, err := db.GetEngine(ctx). | ||||
| 		Where("repo_id=? AND org_id=?", 0, 0). | ||||
| 		Where("repo_id=? AND owner_id=?", 0, 0). | ||||
| 		Get(webhook) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  | @ -38,11 +38,11 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh | |||
| 	webhooks := make([]*Webhook, 0, 5) | ||||
| 	if isActive.IsNone() { | ||||
| 		return webhooks, db.GetEngine(ctx). | ||||
| 			Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true). | ||||
| 			Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). | ||||
| 			Find(&webhooks) | ||||
| 	} | ||||
| 	return webhooks, db.GetEngine(ctx). | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). | ||||
| 		Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). | ||||
| 		Find(&webhooks) | ||||
| } | ||||
| 
 | ||||
|  | @ -50,7 +50,7 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh | |||
| func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error { | ||||
| 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||
| 		count, err := db.GetEngine(ctx). | ||||
| 			Where("repo_id=? AND org_id=?", 0, 0). | ||||
| 			Where("repo_id=? AND owner_id=?", 0, 0). | ||||
| 			Delete(&Webhook{ID: id}) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
|  |  | |||
|  | @ -109,13 +109,13 @@ func TestGetWebhookByRepoID(t *testing.T) { | |||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
| 
 | ||||
| func TestGetWebhookByOrgID(t *testing.T) { | ||||
| func TestGetWebhookByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	hook, err := GetWebhookByOrgID(3, 3) | ||||
| 	hook, err := GetWebhookByOwnerID(3, 3) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, int64(3), hook.ID) | ||||
| 
 | ||||
| 	_, err = GetWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	_, err = GetWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
|  | @ -140,9 +140,9 @@ func TestGetWebhooksByRepoID(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetActiveWebhooksByOrgID(t *testing.T) { | ||||
| func TestGetActiveWebhooksByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue}) | ||||
| 	hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue}) | ||||
| 	assert.NoError(t, err) | ||||
| 	if assert.Len(t, hooks, 1) { | ||||
| 		assert.Equal(t, int64(3), hooks[0].ID) | ||||
|  | @ -150,9 +150,9 @@ func TestGetActiveWebhooksByOrgID(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetWebhooksByOrgID(t *testing.T) { | ||||
| func TestGetWebhooksByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3}) | ||||
| 	hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3}) | ||||
| 	assert.NoError(t, err) | ||||
| 	if assert.Len(t, hooks, 1) { | ||||
| 		assert.Equal(t, int64(3), hooks[0].ID) | ||||
|  | @ -181,13 +181,13 @@ func TestDeleteWebhookByRepoID(t *testing.T) { | |||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
| 
 | ||||
| func TestDeleteWebhookByOrgID(t *testing.T) { | ||||
| func TestDeleteWebhookByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OrgID: 3}) | ||||
| 	assert.NoError(t, DeleteWebhookByOrgID(3, 3)) | ||||
| 	unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OrgID: 3}) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) | ||||
| 	assert.NoError(t, DeleteWebhookByOwnerID(3, 3)) | ||||
| 	unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) | ||||
| 
 | ||||
| 	err := DeleteWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	err := DeleteWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
|  |  | |||
|  | @ -821,6 +821,8 @@ remove_account_link = Remove Linked Account | |||
| remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue? | ||||
| remove_account_link_success = The linked account has been removed. | ||||
| 
 | ||||
| hooks.desc = Add webhooks which will be triggered for <strong>all repositories</strong> owned by this user. | ||||
| 
 | ||||
| orgs_none = You are not a member of any organizations. | ||||
| repos_none = You do not own any repositories | ||||
| 
 | ||||
|  |  | |||
|  | @ -105,10 +105,7 @@ func CreateHook(ctx *context.APIContext) { | |||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	form := web.GetForm(ctx).(*api.CreateHookOption) | ||||
| 	// TODO in body params
 | ||||
| 	if !utils.CheckCreateHookOption(ctx, form) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	utils.AddSystemHook(ctx, form) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -835,6 +835,13 @@ func Routes(ctx gocontext.Context) *web.Route { | |||
| 			m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches) | ||||
| 			m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos) | ||||
| 			m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams) | ||||
| 			m.Group("/hooks", func() { | ||||
| 				m.Combo("").Get(user.ListHooks). | ||||
| 					Post(bind(api.CreateHookOption{}), user.CreateHook) | ||||
| 				m.Combo("/{id}").Get(user.GetHook). | ||||
| 					Patch(bind(api.EditHookOption{}), user.EditHook). | ||||
| 					Delete(user.DeleteHook) | ||||
| 			}, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled()) | ||||
| 		}, reqToken("")) | ||||
| 
 | ||||
| 		// Repositories
 | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ package org | |||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
|  | @ -39,34 +38,10 @@ func ListHooks(ctx *context.APIContext) { | |||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/HookList"
 | ||||
| 
 | ||||
| 	opts := &webhook_model.ListWebhookOptions{ | ||||
| 		ListOptions: utils.GetListOptions(ctx), | ||||
| 		OrgID:       ctx.Org.Organization.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	count, err := webhook_model.CountWebhooksByOpts(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	hooks := make([]*api.Hook, len(orgHooks)) | ||||
| 	for i, hook := range orgHooks { | ||||
| 		hooks[i], err = webhook_service.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.SetTotalCountHeader(count) | ||||
| 	ctx.JSON(http.StatusOK, hooks) | ||||
| 	utils.ListOwnerHooks( | ||||
| 		ctx, | ||||
| 		ctx.ContextUser, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetHook get an organization's hook by id
 | ||||
|  | @ -92,14 +67,12 @@ func GetHook(ctx *context.APIContext) { | |||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	org := ctx.Org.Organization | ||||
| 	hookID := ctx.ParamsInt64(":id") | ||||
| 	hook, err := utils.GetOrgHook(ctx, org.ID, hookID) | ||||
| 	hook, err := utils.GetOwnerHook(ctx, ctx.ContextUser.ID, ctx.ParamsInt64("id")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiHook, err := webhook_service.ToHook(org.AsUser().HomeLink(), hook) | ||||
| 	apiHook, err := webhook_service.ToHook(ctx.ContextUser.HomeLink(), hook) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
|  | @ -131,15 +104,14 @@ func CreateHook(ctx *context.APIContext) { | |||
| 	//   "201":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	form := web.GetForm(ctx).(*api.CreateHookOption) | ||||
| 	// TODO in body params
 | ||||
| 	if !utils.CheckCreateHookOption(ctx, form) { | ||||
| 		return | ||||
| 	} | ||||
| 	utils.AddOrgHook(ctx, form) | ||||
| 	utils.AddOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.ContextUser, | ||||
| 		web.GetForm(ctx).(*api.CreateHookOption), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // EditHook modify a hook of a repository
 | ||||
| // EditHook modify a hook of an organization
 | ||||
| func EditHook(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /orgs/{org}/hooks/{id} organization orgEditHook
 | ||||
| 	// ---
 | ||||
|  | @ -168,11 +140,12 @@ func EditHook(ctx *context.APIContext) { | |||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	form := web.GetForm(ctx).(*api.EditHookOption) | ||||
| 
 | ||||
| 	// TODO in body params
 | ||||
| 	hookID := ctx.ParamsInt64(":id") | ||||
| 	utils.EditOrgHook(ctx, form, hookID) | ||||
| 	utils.EditOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.ContextUser, | ||||
| 		web.GetForm(ctx).(*api.EditHookOption), | ||||
| 		ctx.ParamsInt64("id"), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // DeleteHook delete a hook of an organization
 | ||||
|  | @ -198,15 +171,9 @@ func DeleteHook(ctx *context.APIContext) { | |||
| 	//   "204":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 
 | ||||
| 	org := ctx.Org.Organization | ||||
| 	hookID := ctx.ParamsInt64(":id") | ||||
| 	if err := webhook_model.DeleteWebhookByOrgID(org.ID, hookID); err != nil { | ||||
| 		if webhook_model.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOrgID", err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| 	utils.DeleteOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.ContextUser, | ||||
| 		ctx.ParamsInt64("id"), | ||||
| 	) | ||||
| } | ||||
|  |  | |||
|  | @ -223,12 +223,8 @@ func CreateHook(ctx *context.APIContext) { | |||
| 	// responses:
 | ||||
| 	//   "201":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 	form := web.GetForm(ctx).(*api.CreateHookOption) | ||||
| 
 | ||||
| 	if !utils.CheckCreateHookOption(ctx, form) { | ||||
| 		return | ||||
| 	} | ||||
| 	utils.AddRepoHook(ctx, form) | ||||
| 	utils.AddRepoHook(ctx, web.GetForm(ctx).(*api.CreateHookOption)) | ||||
| } | ||||
| 
 | ||||
| // EditHook modify a hook of a repository
 | ||||
|  |  | |||
|  | @ -0,0 +1,154 @@ | |||
| // Copyright 2022 The Gitea Authors. All rights reserved.
 | ||||
| // SPDX-License-Identifier: MIT
 | ||||
| 
 | ||||
| package user | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| 	webhook_service "code.gitea.io/gitea/services/webhook" | ||||
| ) | ||||
| 
 | ||||
| // ListHooks list the authenticated user's webhooks
 | ||||
| func ListHooks(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /user/hooks user userListHooks
 | ||||
| 	// ---
 | ||||
| 	// summary: List the authenticated user's webhooks
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - 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/HookList"
 | ||||
| 
 | ||||
| 	utils.ListOwnerHooks( | ||||
| 		ctx, | ||||
| 		ctx.Doer, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // GetHook get the authenticated user's hook by id
 | ||||
| func GetHook(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /user/hooks/{id} user userGetHook
 | ||||
| 	// ---
 | ||||
| 	// summary: Get a hook
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: id
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of the hook to get
 | ||||
| 	//   type: integer
 | ||||
| 	//   format: int64
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	hook, err := utils.GetOwnerHook(ctx, ctx.Doer.ID, ctx.ParamsInt64("id")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, apiHook) | ||||
| } | ||||
| 
 | ||||
| // CreateHook create a hook for the authenticated user
 | ||||
| func CreateHook(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /user/hooks user userCreateHook
 | ||||
| 	// ---
 | ||||
| 	// summary: Create a hook
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: body
 | ||||
| 	//   in: body
 | ||||
| 	//   required: true
 | ||||
| 	//   schema:
 | ||||
| 	//     "$ref": "#/definitions/CreateHookOption"
 | ||||
| 	// responses:
 | ||||
| 	//   "201":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	utils.AddOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.Doer, | ||||
| 		web.GetForm(ctx).(*api.CreateHookOption), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // EditHook modify a hook of the authenticated user
 | ||||
| func EditHook(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /user/hooks/{id} user userEditHook
 | ||||
| 	// ---
 | ||||
| 	// summary: Update a hook
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: id
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of the hook to update
 | ||||
| 	//   type: integer
 | ||||
| 	//   format: int64
 | ||||
| 	//   required: true
 | ||||
| 	// - name: body
 | ||||
| 	//   in: body
 | ||||
| 	//   schema:
 | ||||
| 	//     "$ref": "#/definitions/EditHookOption"
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Hook"
 | ||||
| 
 | ||||
| 	utils.EditOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.Doer, | ||||
| 		web.GetForm(ctx).(*api.EditHookOption), | ||||
| 		ctx.ParamsInt64("id"), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // DeleteHook delete a hook of the authenticated user
 | ||||
| func DeleteHook(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /user/hooks/{id} user userDeleteHook
 | ||||
| 	// ---
 | ||||
| 	// summary: Delete a hook
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: id
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of the hook to delete
 | ||||
| 	//   type: integer
 | ||||
| 	//   format: int64
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "204":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 
 | ||||
| 	utils.DeleteOwnerHook( | ||||
| 		ctx, | ||||
| 		ctx.Doer, | ||||
| 		ctx.ParamsInt64("id"), | ||||
| 	) | ||||
| } | ||||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
|  | @ -18,15 +19,46 @@ import ( | |||
| 	webhook_service "code.gitea.io/gitea/services/webhook" | ||||
| ) | ||||
| 
 | ||||
| // GetOrgHook get an organization's webhook. If there is an error, write to
 | ||||
| // `ctx` accordingly and return the error
 | ||||
| func GetOrgHook(ctx *context.APIContext, orgID, hookID int64) (*webhook.Webhook, error) { | ||||
| 	w, err := webhook.GetWebhookByOrgID(orgID, hookID) | ||||
| // ListOwnerHooks lists the webhooks of the provided owner
 | ||||
| func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { | ||||
| 	opts := &webhook.ListWebhookOptions{ | ||||
| 		ListOptions: GetListOptions(ctx), | ||||
| 		OwnerID:     owner.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	count, err := webhook.CountWebhooksByOpts(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	hooks, err := webhook.ListWebhooksByOpts(ctx, opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiHooks := make([]*api.Hook, len(hooks)) | ||||
| 	for i, hook := range hooks { | ||||
| 		apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.SetTotalCountHeader(count) | ||||
| 	ctx.JSON(http.StatusOK, apiHooks) | ||||
| } | ||||
| 
 | ||||
| // GetOwnerHook gets an user or organization webhook. Errors are written to ctx.
 | ||||
| func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { | ||||
| 	w, err := webhook.GetWebhookByOwnerID(ownerID, hookID) | ||||
| 	if err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetWebhookByOrgID", err) | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err) | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -48,9 +80,9 @@ func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhoo | |||
| 	return w, nil | ||||
| } | ||||
| 
 | ||||
| // CheckCreateHookOption check if a CreateHookOption form is valid. If invalid,
 | ||||
| // checkCreateHookOption check if a CreateHookOption form is valid. If invalid,
 | ||||
| // write the appropriate error to `ctx`. Return whether the form is valid
 | ||||
| func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { | ||||
| func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { | ||||
| 	if !webhook_service.IsValidHookTaskType(form.Type) { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type)) | ||||
| 		return false | ||||
|  | @ -81,14 +113,13 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AddOrgHook add a hook to an organization. Writes to `ctx` accordingly
 | ||||
| func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) { | ||||
| 	org := ctx.Org.Organization | ||||
| 	hook, ok := addHook(ctx, form, org.ID, 0) | ||||
| // AddOwnerHook adds a hook to an user or organization
 | ||||
| func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { | ||||
| 	hook, ok := addHook(ctx, form, owner.ID, 0) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
| 	apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), hook) | ||||
| 	apiHook, ok := toAPIHook(ctx, owner.HomeLink(), hook) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | @ -128,14 +159,18 @@ func pullHook(events []string, event string) bool { | |||
| 	return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true) | ||||
| } | ||||
| 
 | ||||
| // addHook add the hook specified by `form`, `orgID` and `repoID`. If there is
 | ||||
| // addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is
 | ||||
| // an error, write to `ctx` accordingly. Return (webhook, ok)
 | ||||
| func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID int64) (*webhook.Webhook, bool) { | ||||
| func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { | ||||
| 	if !checkCreateHookOption(ctx, form) { | ||||
| 		return nil, false | ||||
| 	} | ||||
| 
 | ||||
| 	if len(form.Events) == 0 { | ||||
| 		form.Events = []string{"push"} | ||||
| 	} | ||||
| 	w := &webhook.Webhook{ | ||||
| 		OrgID:       orgID, | ||||
| 		OwnerID:     ownerID, | ||||
| 		RepoID:      repoID, | ||||
| 		URL:         form.Config["url"], | ||||
| 		ContentType: webhook.ToHookContentType(form.Config["content_type"]), | ||||
|  | @ -234,21 +269,20 @@ func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID in | |||
| 	ctx.JSON(http.StatusOK, h) | ||||
| } | ||||
| 
 | ||||
| // EditOrgHook edit webhook `w` according to `form`. Writes to `ctx` accordingly
 | ||||
| func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { | ||||
| 	org := ctx.Org.Organization | ||||
| 	hook, err := GetOrgHook(ctx, org.ID, hookID) | ||||
| // EditOwnerHook updates a webhook of an user or organization
 | ||||
| func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) { | ||||
| 	hook, err := GetOwnerHook(ctx, owner.ID, hookID) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if !editHook(ctx, form, hook) { | ||||
| 		return | ||||
| 	} | ||||
| 	updated, err := GetOrgHook(ctx, org.ID, hookID) | ||||
| 	updated, err := GetOwnerHook(ctx, owner.ID, hookID) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), updated) | ||||
| 	apiHook, ok := toAPIHook(ctx, owner.HomeLink(), updated) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | @ -362,3 +396,16 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh | |||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // DeleteOwnerHook deletes the hook owned by the owner.
 | ||||
| func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(owner.ID, hookID); err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  |  | |||
|  | @ -218,9 +218,9 @@ func Webhooks(ctx *context.Context) { | |||
| 	ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks" | ||||
| 	ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc") | ||||
| 
 | ||||
| 	ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) | ||||
| 	ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetWebhooksByOrgId", err) | ||||
| 		ctx.ServerError("ListWebhooksByOpts", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  | @ -230,8 +230,8 @@ func Webhooks(ctx *context.Context) { | |||
| 
 | ||||
| // DeleteWebhook response for delete webhook
 | ||||
| func DeleteWebhook(ctx *context.Context) { | ||||
| 	if err := webhook.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error()) | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
| 	} | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ const ( | |||
| 	tplHooks        base.TplName = "repo/settings/webhook/base" | ||||
| 	tplHookNew      base.TplName = "repo/settings/webhook/new" | ||||
| 	tplOrgHookNew   base.TplName = "org/settings/hook_new" | ||||
| 	tplUserHookNew  base.TplName = "user/settings/hook_new" | ||||
| 	tplAdminHookNew base.TplName = "admin/hook_new" | ||||
| ) | ||||
| 
 | ||||
|  | @ -54,8 +55,8 @@ func Webhooks(ctx *context.Context) { | |||
| 	ctx.HTML(http.StatusOK, tplHooks) | ||||
| } | ||||
| 
 | ||||
| type orgRepoCtx struct { | ||||
| 	OrgID           int64 | ||||
| type ownerRepoCtx struct { | ||||
| 	OwnerID         int64 | ||||
| 	RepoID          int64 | ||||
| 	IsAdmin         bool | ||||
| 	IsSystemWebhook bool | ||||
|  | @ -64,10 +65,10 @@ type orgRepoCtx struct { | |||
| 	NewTemplate     base.TplName | ||||
| } | ||||
| 
 | ||||
| // getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context.
 | ||||
| func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { | ||||
| 	if len(ctx.Repo.RepoLink) > 0 { | ||||
| 		return &orgRepoCtx{ | ||||
| // getOwnerRepoCtx determines whether this is a repo, owner, or admin (both default and system) context.
 | ||||
| func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { | ||||
| 	if is, ok := ctx.Data["IsRepositoryWebhook"]; ok && is.(bool) { | ||||
| 		return &ownerRepoCtx{ | ||||
| 			RepoID:      ctx.Repo.Repository.ID, | ||||
| 			Link:        path.Join(ctx.Repo.RepoLink, "settings/hooks"), | ||||
| 			LinkNew:     path.Join(ctx.Repo.RepoLink, "settings/hooks"), | ||||
|  | @ -75,37 +76,35 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { | |||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if len(ctx.Org.OrgLink) > 0 { | ||||
| 		return &orgRepoCtx{ | ||||
| 			OrgID:       ctx.Org.Organization.ID, | ||||
| 	if is, ok := ctx.Data["IsOrganizationWebhook"]; ok && is.(bool) { | ||||
| 		return &ownerRepoCtx{ | ||||
| 			OwnerID:     ctx.ContextUser.ID, | ||||
| 			Link:        path.Join(ctx.Org.OrgLink, "settings/hooks"), | ||||
| 			LinkNew:     path.Join(ctx.Org.OrgLink, "settings/hooks"), | ||||
| 			NewTemplate: tplOrgHookNew, | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if ctx.Doer.IsAdmin { | ||||
| 		// Are we looking at default webhooks?
 | ||||
| 		if ctx.Params(":configType") == "default-hooks" { | ||||
| 			return &orgRepoCtx{ | ||||
| 				IsAdmin:     true, | ||||
| 				Link:        path.Join(setting.AppSubURL, "/admin/hooks"), | ||||
| 				LinkNew:     path.Join(setting.AppSubURL, "/admin/default-hooks"), | ||||
| 				NewTemplate: tplAdminHookNew, | ||||
| 	if is, ok := ctx.Data["IsUserWebhook"]; ok && is.(bool) { | ||||
| 		return &ownerRepoCtx{ | ||||
| 			OwnerID:     ctx.Doer.ID, | ||||
| 			Link:        path.Join(setting.AppSubURL, "/user/settings/hooks"), | ||||
| 			LinkNew:     path.Join(setting.AppSubURL, "/user/settings/hooks"), | ||||
| 			NewTemplate: tplUserHookNew, | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 		// Must be system webhooks instead
 | ||||
| 		return &orgRepoCtx{ | ||||
| 	if ctx.Doer.IsAdmin { | ||||
| 		return &ownerRepoCtx{ | ||||
| 			IsAdmin:         true, | ||||
| 			IsSystemWebhook: true, | ||||
| 			IsSystemWebhook: ctx.Params(":configType") == "system-hooks", | ||||
| 			Link:            path.Join(setting.AppSubURL, "/admin/hooks"), | ||||
| 			LinkNew:         path.Join(setting.AppSubURL, "/admin/system-hooks"), | ||||
| 			NewTemplate:     tplAdminHookNew, | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, errors.New("unable to set OrgRepo context") | ||||
| 	return nil, errors.New("unable to set OwnerRepo context") | ||||
| } | ||||
| 
 | ||||
| func checkHookType(ctx *context.Context) string { | ||||
|  | @ -122,9 +121,9 @@ func WebhooksNew(ctx *context.Context) { | |||
| 	ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") | ||||
| 	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} | ||||
| 
 | ||||
| 	orCtx, err := getOrgRepoCtx(ctx) | ||||
| 	orCtx, err := getOwnerRepoCtx(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("getOrgRepoCtx", err) | ||||
| 		ctx.ServerError("getOwnerRepoCtx", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  | @ -205,9 +204,9 @@ func createWebhook(ctx *context.Context, params webhookParams) { | |||
| 	ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} | ||||
| 	ctx.Data["HookType"] = params.Type | ||||
| 
 | ||||
| 	orCtx, err := getOrgRepoCtx(ctx) | ||||
| 	orCtx, err := getOwnerRepoCtx(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("getOrgRepoCtx", err) | ||||
| 		ctx.ServerError("getOwnerRepoCtx", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["BaseLink"] = orCtx.LinkNew | ||||
|  | @ -236,7 +235,7 @@ func createWebhook(ctx *context.Context, params webhookParams) { | |||
| 		IsActive:        params.WebhookForm.Active, | ||||
| 		Type:            params.Type, | ||||
| 		Meta:            string(meta), | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		OwnerID:         orCtx.OwnerID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader) | ||||
|  | @ -577,19 +576,19 @@ func packagistHookParams(ctx *context.Context) webhookParams { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { | ||||
| 	orCtx, err := getOrgRepoCtx(ctx) | ||||
| func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { | ||||
| 	orCtx, err := getOwnerRepoCtx(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("getOrgRepoCtx", err) | ||||
| 		ctx.ServerError("getOwnerRepoCtx", err) | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	ctx.Data["BaseLink"] = orCtx.Link | ||||
| 
 | ||||
| 	var w *webhook.Webhook | ||||
| 	if orCtx.RepoID > 0 { | ||||
| 		w, err = webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.OrgID > 0 { | ||||
| 		w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) | ||||
| 		w, err = webhook.GetWebhookByRepoID(orCtx.RepoID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.OwnerID > 0 { | ||||
| 		w, err = webhook.GetWebhookByOwnerID(orCtx.OwnerID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.IsAdmin { | ||||
| 		w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id")) | ||||
| 	} | ||||
|  |  | |||
|  | @ -0,0 +1,48 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | ||||
| // SPDX-License-Identifier: MIT
 | ||||
| 
 | ||||
| package setting | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	tplSettingsHooks base.TplName = "user/settings/hooks" | ||||
| ) | ||||
| 
 | ||||
| // Webhooks render webhook list page
 | ||||
| func Webhooks(ctx *context.Context) { | ||||
| 	ctx.Data["Title"] = ctx.Tr("settings") | ||||
| 	ctx.Data["PageIsSettingsHooks"] = true | ||||
| 	ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" | ||||
| 	ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" | ||||
| 	ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") | ||||
| 
 | ||||
| 	ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("ListWebhooksByOpts", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Data["Webhooks"] = ws | ||||
| 	ctx.HTML(http.StatusOK, tplSettingsHooks) | ||||
| } | ||||
| 
 | ||||
| // DeleteWebhook response for delete webhook
 | ||||
| func DeleteWebhook(ctx *context.Context) { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx.Doer.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
| 		"redirect": setting.AppSubURL + "/user/settings/hooks", | ||||
| 	}) | ||||
| } | ||||
|  | @ -315,6 +315,35 @@ func RegisterRoutes(m *web.Route) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	addWebhookAddRoutes := func() { | ||||
| 		m.Get("/{type}/new", repo.WebhooksNew) | ||||
| 		m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 		m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 		m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 		m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| 		m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) | ||||
| 		m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) | ||||
| 		m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) | ||||
| 		m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) | ||||
| 		m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) | ||||
| 		m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) | ||||
| 		m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) | ||||
| 	} | ||||
| 
 | ||||
| 	addWebhookEditRoutes := func() { | ||||
| 		m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) | ||||
| 		m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) | ||||
| 		m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) | ||||
| 		m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) | ||||
| 		m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) | ||||
| 		m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) | ||||
| 		m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) | ||||
| 		m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) | ||||
| 		m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) | ||||
| 		m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) | ||||
| 		m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) | ||||
| 	} | ||||
| 
 | ||||
| 	// FIXME: not all routes need go through same middleware.
 | ||||
| 	// Especially some AJAX requests, we can reduce middleware number to improve performance.
 | ||||
| 	// Routers.
 | ||||
|  | @ -482,6 +511,19 @@ func RegisterRoutes(m *web.Route) { | |||
| 		m.Get("/organization", user_setting.Organization) | ||||
| 		m.Get("/repos", user_setting.Repos) | ||||
| 		m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository) | ||||
| 
 | ||||
| 		m.Group("/hooks", func() { | ||||
| 			m.Get("", user_setting.Webhooks) | ||||
| 			m.Post("/delete", user_setting.DeleteWebhook) | ||||
| 			addWebhookAddRoutes() | ||||
| 			m.Group("/{id}", func() { | ||||
| 				m.Get("", repo.WebHooksEdit) | ||||
| 				m.Post("/replay/{uuid}", repo.ReplayWebhook) | ||||
| 			}) | ||||
| 			addWebhookEditRoutes() | ||||
| 		}, webhooksEnabled, func(ctx *context.Context) { | ||||
| 			ctx.Data["IsUserWebhook"] = true | ||||
| 		}) | ||||
| 	}, reqSignIn, func(ctx *context.Context) { | ||||
| 		ctx.Data["PageIsUserSettings"] = true | ||||
| 		ctx.Data["AllThemes"] = setting.UI.Themes | ||||
|  | @ -575,32 +617,11 @@ func RegisterRoutes(m *web.Route) { | |||
| 				m.Get("", repo.WebHooksEdit) | ||||
| 				m.Post("/replay/{uuid}", repo.ReplayWebhook) | ||||
| 			}) | ||||
| 			m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) | ||||
| 			m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) | ||||
| 			m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) | ||||
| 			m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) | ||||
| 			m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) | ||||
| 			m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) | ||||
| 			m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) | ||||
| 			m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) | ||||
| 			m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) | ||||
| 			m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) | ||||
| 			m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) | ||||
| 			addWebhookEditRoutes() | ||||
| 		}, webhooksEnabled) | ||||
| 
 | ||||
| 		m.Group("/{configType:default-hooks|system-hooks}", func() { | ||||
| 			m.Get("/{type}/new", repo.WebhooksNew) | ||||
| 			m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 			m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 			m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 			m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| 			m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) | ||||
| 			m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) | ||||
| 			m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) | ||||
| 			m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) | ||||
| 			m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) | ||||
| 			m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) | ||||
| 			m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) | ||||
| 			addWebhookAddRoutes() | ||||
| 		}) | ||||
| 
 | ||||
| 		m.Group("/auths", func() { | ||||
|  | @ -759,32 +780,15 @@ func RegisterRoutes(m *web.Route) { | |||
| 				m.Group("/hooks", func() { | ||||
| 					m.Get("", org.Webhooks) | ||||
| 					m.Post("/delete", org.DeleteWebhook) | ||||
| 					m.Get("/{type}/new", repo.WebhooksNew) | ||||
| 					m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 					m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 					m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 					m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| 					m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) | ||||
| 					m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) | ||||
| 					m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) | ||||
| 					m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) | ||||
| 					m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) | ||||
| 					m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) | ||||
| 					addWebhookAddRoutes() | ||||
| 					m.Group("/{id}", func() { | ||||
| 						m.Get("", repo.WebHooksEdit) | ||||
| 						m.Post("/replay/{uuid}", repo.ReplayWebhook) | ||||
| 					}) | ||||
| 					m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) | ||||
| 					m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) | ||||
| 					m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) | ||||
| 					m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) | ||||
| 					m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) | ||||
| 					m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) | ||||
| 					m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) | ||||
| 					m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) | ||||
| 					m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) | ||||
| 					m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) | ||||
| 				}, webhooksEnabled) | ||||
| 					addWebhookEditRoutes() | ||||
| 				}, webhooksEnabled, func(ctx *context.Context) { | ||||
| 					ctx.Data["IsOrganizationWebhook"] = true | ||||
| 				}) | ||||
| 
 | ||||
| 				m.Group("/labels", func() { | ||||
| 					m.Get("", org.RetrieveLabels, org.Labels) | ||||
|  | @ -962,35 +966,16 @@ func RegisterRoutes(m *web.Route) { | |||
| 			m.Group("/hooks", func() { | ||||
| 				m.Get("", repo.Webhooks) | ||||
| 				m.Post("/delete", repo.DeleteWebhook) | ||||
| 				m.Get("/{type}/new", repo.WebhooksNew) | ||||
| 				m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 				m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 				m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 				m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| 				m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) | ||||
| 				m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) | ||||
| 				m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) | ||||
| 				m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) | ||||
| 				m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) | ||||
| 				m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) | ||||
| 				m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) | ||||
| 				addWebhookAddRoutes() | ||||
| 				m.Group("/{id}", func() { | ||||
| 					m.Get("", repo.WebHooksEdit) | ||||
| 					m.Post("/test", repo.TestWebhook) | ||||
| 					m.Post("/replay/{uuid}", repo.ReplayWebhook) | ||||
| 				}) | ||||
| 				m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) | ||||
| 				m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) | ||||
| 				m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) | ||||
| 				m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) | ||||
| 				m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) | ||||
| 				m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) | ||||
| 				m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) | ||||
| 				m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) | ||||
| 				m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) | ||||
| 				m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) | ||||
| 				m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) | ||||
| 			}, webhooksEnabled) | ||||
| 				addWebhookEditRoutes() | ||||
| 			}, webhooksEnabled, func(ctx *context.Context) { | ||||
| 				ctx.Data["IsRepositoryWebhook"] = true | ||||
| 			}) | ||||
| 
 | ||||
| 			m.Group("/keys", func() { | ||||
| 				m.Combo("").Get(repo.DeployKeys). | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_mode | |||
| 			HookEvent:   templateWebhook.HookEvent, | ||||
| 			IsActive:    templateWebhook.IsActive, | ||||
| 			Type:        templateWebhook.Type, | ||||
| 			OrgID:       templateWebhook.OrgID, | ||||
| 			OwnerID:     templateWebhook.OwnerID, | ||||
| 			Events:      templateWebhook.Events, | ||||
| 			Meta:        templateWebhook.Meta, | ||||
| 		}) | ||||
|  |  | |||
|  | @ -229,16 +229,16 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu | |||
| 		owner = source.Repository.MustOwner(ctx) | ||||
| 	} | ||||
| 
 | ||||
| 	// check if owner is an org and append additional webhooks
 | ||||
| 	if owner != nil && owner.IsOrganization() { | ||||
| 		orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ | ||||
| 			OrgID:    owner.ID, | ||||
| 	// append additional webhooks of a user or organization
 | ||||
| 	if owner != nil { | ||||
| 		ownerHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ | ||||
| 			OwnerID:  owner.ID, | ||||
| 			IsActive: util.OptionalBoolTrue, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("ListWebhooksByOpts: %w", err) | ||||
| 		} | ||||
| 		ws = append(ws, orgHooks...) | ||||
| 		ws = append(ws, ownerHooks...) | ||||
| 	} | ||||
| 
 | ||||
| 	// Add any admin-defined system webhooks
 | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ | |||
| 									<span class="ui label">N/A</span> | ||||
| 								{{end}} | ||||
| 							</a> | ||||
| 							{{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin}} | ||||
| 							{{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin $.PageIsUserSettings}} | ||||
| 							<div class="right menu"> | ||||
| 								<form class="item" action="{{$.Link}}/replay/{{.UUID}}" method="post"> | ||||
| 									{{$.CsrfTokenHtml}} | ||||
|  |  | |||
|  | @ -13014,6 +13014,152 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/hooks": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "List the authenticated user's webhooks", | ||||
|         "operationId": "userListHooks", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "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" | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/HookList" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "post": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Create a hook", | ||||
|         "operationId": "userCreateHook", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "body", | ||||
|             "in": "body", | ||||
|             "required": true, | ||||
|             "schema": { | ||||
|               "$ref": "#/definitions/CreateHookOption" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "201": { | ||||
|             "$ref": "#/responses/Hook" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/hooks/{id}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Get a hook", | ||||
|         "operationId": "userGetHook", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "integer", | ||||
|             "format": "int64", | ||||
|             "description": "id of the hook to get", | ||||
|             "name": "id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/Hook" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Delete a hook", | ||||
|         "operationId": "userDeleteHook", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "integer", | ||||
|             "format": "int64", | ||||
|             "description": "id of the hook to delete", | ||||
|             "name": "id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "patch": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Update a hook", | ||||
|         "operationId": "userEditHook", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "integer", | ||||
|             "format": "int64", | ||||
|             "description": "id of the hook to update", | ||||
|             "name": "id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "name": "body", | ||||
|             "in": "body", | ||||
|             "schema": { | ||||
|               "$ref": "#/definitions/EditHookOption" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/Hook" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/keys": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|  |  | |||
|  | @ -138,6 +138,12 @@ | |||
| 							<label>admin:org_hook</label> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<div class="ui checkbox"> | ||||
| 							<input class="enable-system" type="checkbox" name="scope" value="admin:user_hook"> | ||||
| 							<label>admin:user_hook</label> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<div class="ui checkbox"> | ||||
| 							<input class="enable-system" type="checkbox" name="scope" value="notification"> | ||||
|  |  | |||
|  | @ -0,0 +1,53 @@ | |||
| {{template "base/head" .}} | ||||
| <div class="page-content user  settings new webhook"> | ||||
| 	{{template "user/settings/navbar" .}} | ||||
| 	<div class="ui container"> | ||||
| 		<div class="twelve wide column content"> | ||||
| 			{{template "base/alert" .}} | ||||
| 			<h4 class="ui top attached header"> | ||||
| 				{{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}} | ||||
| 				<div class="ui right"> | ||||
| 					{{if eq .HookType "gitea"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gitea.svg"> | ||||
| 					{{else if eq .HookType "gogs"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/gogs.ico"> | ||||
| 					{{else if eq .HookType "slack"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/slack.png"> | ||||
| 					{{else if eq .HookType "discord"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/discord.png"> | ||||
| 					{{else if eq .HookType "dingtalk"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/dingtalk.ico"> | ||||
| 					{{else if eq .HookType "telegram"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/telegram.png"> | ||||
| 					{{else if eq .HookType "msteams"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/msteams.png"> | ||||
| 					{{else if eq .HookType "feishu"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/feishu.png"> | ||||
| 					{{else if eq .HookType "matrix"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/matrix.svg"> | ||||
| 					{{else if eq .HookType "wechatwork"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/wechatwork.png"> | ||||
| 					{{else if eq .HookType "packagist"}} | ||||
| 						<img width="26" height="26" src="{{AssetUrlPrefix}}/img/packagist.png"> | ||||
| 					{{end}} | ||||
| 				</div> | ||||
| 			</h4> | ||||
| 			<div class="ui attached segment"> | ||||
| 				{{template "repo/settings/webhook/gitea" .}} | ||||
| 				{{template "repo/settings/webhook/gogs" .}} | ||||
| 				{{template "repo/settings/webhook/slack" .}} | ||||
| 				{{template "repo/settings/webhook/discord" .}} | ||||
| 				{{template "repo/settings/webhook/dingtalk" .}} | ||||
| 				{{template "repo/settings/webhook/telegram" .}} | ||||
| 				{{template "repo/settings/webhook/msteams" .}} | ||||
| 				{{template "repo/settings/webhook/feishu" .}} | ||||
| 				{{template "repo/settings/webhook/matrix" .}} | ||||
| 				{{template "repo/settings/webhook/wechatwork" .}} | ||||
| 				{{template "repo/settings/webhook/packagist" .}} | ||||
| 			</div> | ||||
| 
 | ||||
| 			{{template "repo/settings/webhook/history" .}} | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| {{template "base/footer" .}} | ||||
|  | @ -0,0 +1,8 @@ | |||
| {{template "base/head" .}} | ||||
| <div class="page-content user settings webhooks"> | ||||
| 	{{template "user/settings/navbar" .}} | ||||
| 	<div class="ui container"> | ||||
| 		{{template "repo/settings/webhook/list" .}} | ||||
| 	</div> | ||||
| </div> | ||||
| {{template "base/footer" .}} | ||||
|  | @ -26,6 +26,11 @@ | |||
| 			{{.locale.Tr "packages.title"}} | ||||
| 		</a> | ||||
| 		{{end}} | ||||
| 		{{if not DisableWebhooks}} | ||||
| 		<a class="{{if .PageIsSettingsHooks}}active {{end}}item" href="{{AppSubUrl}}/user/settings/hooks"> | ||||
| 			{{.locale.Tr "repo.settings.hooks"}} | ||||
| 		</a> | ||||
| 		{{end}} | ||||
| 		<a class="{{if .PageIsSettingsOrganization}}active {{end}}item" href="{{AppSubUrl}}/user/settings/organization"> | ||||
| 			{{.locale.Tr "settings.organization"}} | ||||
| 		</a> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue