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. ![grafik](https://user-images.githubusercontent.com/1666336/197391408-15dfdc23-b476-4d0c-82f7-9bc9b065988f.png)tags/v1.20.0-rc0
@@ -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, | |||
ID: id, | |||
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, | |||
ID: id, | |||
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, | |||
}, nil | |||
} | |||
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> |