From a3c56ace0f2c809eb7faac062a3045f47cfcceaf Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 23 Oct 2022 11:27:32 +0000 Subject: [PATCH 1/9] Add user webhooks. --- models/migrations/migrations.go | 2 + models/migrations/v229.go | 35 +++++ models/webhook/webhook.go | 34 ++--- options/locale/locale_en-US.ini | 2 + routers/api/v1/api.go | 8 + routers/api/v1/org/hook.go | 75 +++------- routers/api/v1/repo/hook.go | 6 +- routers/api/v1/user/hook.go | 149 +++++++++++++++++++ routers/api/v1/utils/hook.go | 80 +++++++--- routers/web/org/setting.go | 8 +- routers/web/repo/webhook.go | 67 ++++----- routers/web/user/setting/webhooks.go | 49 ++++++ routers/web/web.go | 44 +++++- services/repository/hooks.go | 2 +- services/webhook/webhook.go | 10 +- templates/repo/settings/webhook/history.tmpl | 2 +- templates/swagger/v1_json.tmpl | 146 ++++++++++++++++++ templates/user/settings/hook_new.tmpl | 53 +++++++ templates/user/settings/hooks.tmpl | 8 + templates/user/settings/navbar.tmpl | 3 + 20 files changed, 643 insertions(+), 140 deletions(-) create mode 100644 models/migrations/v229.go create mode 100644 routers/api/v1/user/hook.go create mode 100644 routers/web/user/setting/webhooks.go create mode 100644 templates/user/settings/hook_new.tmpl create mode 100644 templates/user/settings/hooks.tmpl diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 46ef052829b6d..011e6bd1d68c3 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -419,6 +419,8 @@ var migrations = []Migration{ NewMigration("Create key/value table for system settings", createSystemSettingsTable), // v228 -> v229 NewMigration("Add TeamInvite table", addTeamInviteTable), + // v229 -> v230 + NewMigration("rename Webhook org_id to owner_id", renameWebhookOrgToOwner), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v229.go b/models/migrations/v229.go new file mode 100644 index 0000000000000..d3049a5853d50 --- /dev/null +++ b/models/migrations/v229.go @@ -0,0 +1,35 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func renameWebhookOrgToOwner(x *xorm.Engine) error { + type Webhook struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 + OwnerID int64 `xorm:"INDEX"` + } + + 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 _, err := sess.Exec("UPDATE webhook SET owner_id = org_id"); err != nil { + return err + } + if err := dropTableColumns(sess, "webhook", "org_id"); err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 4551dcff5fb18..9c326b5dfa034 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -182,7 +182,7 @@ const ( 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"` @@ -446,11 +446,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, }) } @@ -458,7 +458,7 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { type ListWebhookOptions struct { db.ListOptions RepoID int64 - OrgID int64 + OwnerID int64 IsActive util.OptionalBool } @@ -467,8 +467,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()}) @@ -501,7 +501,7 @@ func CountWebhooksByOpts(opts *ListWebhookOptions) (int64, error) { 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) } @@ -509,7 +509,7 @@ func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { func GetSystemOrDefaultWebhook(id int64) (*Webhook, error) { webhook := &Webhook{ID: id} has, err := db.GetEngine(db.DefaultContext). - Where("repo_id=? AND org_id=?", 0, 0). + Where("repo_id=? AND owner_id=?", 0, 0). Get(webhook) if err != nil { return nil, err @@ -524,11 +524,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) } @@ -572,11 +572,11 @@ 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, }) } @@ -589,7 +589,7 @@ func DeleteDefaultSystemWebhook(id int64) error { defer committer.Close() 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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a35c6a668af4a..a90868d1e6b9b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -798,6 +798,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 all repositories under this user. + orgs_none = You are not a member of any organizations. repos_none = You do not own any repositories diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0d11674aa9971..cd4973578eb59 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -774,6 +774,14 @@ func Routes(ctx gocontext.Context) *web.Route { m.Get("/subscriptions", user.GetMyWatchedRepos) m.Get("/teams", 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) + }, reqWebhooksEnabled()) }, reqToken()) // Repositories diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index 2ddb2b2d2ba94..eb43fc073db0f 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -7,7 +7,6 @@ package org import ( "net/http" - "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -40,30 +39,10 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - opts := &webhook.ListWebhookOptions{ - ListOptions: utils.GetListOptions(ctx), - OrgID: ctx.Org.Organization.ID, - } - - count, err := webhook.CountWebhooksByOpts(opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - orgHooks, err := webhook.ListWebhooksByOpts(ctx, opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - hooks := make([]*api.Hook, len(orgHooks)) - for i, hook := range orgHooks { - hooks[i] = convert.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook) - } - - ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, hooks) + utils.ListOwnerHooks( + ctx, + ctx.ContextUser, + ) } // GetHook get an organization's hook by id @@ -89,13 +68,11 @@ 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 } - ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), hook)) + ctx.JSON(http.StatusOK, convert.ToHook(ctx.ContextUser.HomeLink(), hook)) } // CreateHook create a hook for an organization @@ -122,15 +99,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 // --- @@ -159,11 +135,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 @@ -189,15 +166,9 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - org := ctx.Org.Organization - hookID := ctx.ParamsInt64(":id") - if err := webhook.DeleteWebhookByOrgID(org.ID, hookID); err != nil { - if webhook.IsErrWebhookNotExist(err) { - ctx.NotFound() - } else { - ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOrgID", err) - } - return - } - ctx.Status(http.StatusNoContent) + utils.DeleteOwnerHook( + ctx, + ctx.ContextUser, + ctx.ParamsInt64("id"), + ) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 5956fe9da92f6..0d76e35a6ffee 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -214,12 +214,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 diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go new file mode 100644 index 0000000000000..f72006c2b9614 --- /dev/null +++ b/routers/api/v1/user/hook.go @@ -0,0 +1,149 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" +) + +// 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 + } + ctx.JSON(http.StatusOK, convert.ToHook(ctx.Doer.HomeLink(), hook)) +} + +// 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"), + ) +} diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 7e4dfca9adf20..918dff0829008 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -9,6 +9,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/convert" @@ -18,10 +19,37 @@ import ( webhook_service "code.gitea.io/gitea/services/webhook" ) -// GetOrgHook get an organization's webhook. If there is an error, write to +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] = convert.ToHook(owner.HomeLink(), hook) + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiHooks) +} + +// GetOwnerHook get an user or 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) +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() @@ -48,9 +76,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 @@ -68,12 +96,11 @@ func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) return true } -// 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 add a hook to an user or organization. Writes to `ctx` accordingly +func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { + hook, ok := addHook(ctx, form, owner.ID, 0) if ok { - ctx.JSON(http.StatusCreated, convert.ToHook(org.AsUser().HomeLink(), hook)) + ctx.JSON(http.StatusCreated, convert.ToHook(owner.HomeLink(), hook)) } } @@ -94,14 +121,18 @@ func pullHook(events []string, event string) bool { return util.IsStringInSlice(event, events, true) || util.IsStringInSlice(string(webhook.HookEventPullRequest), events, 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"]), @@ -171,21 +202,20 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID return w, true } -// 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 edit webhook `w` according to `form`. Writes to `ctx` accordingly +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 } - ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), updated)) + ctx.JSON(http.StatusOK, convert.ToHook(owner.HomeLink(), updated)) } // EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly @@ -285,3 +315,15 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } return true } + +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) +} diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 3f7bc59856f36..3fb9d8cde4da5 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -215,9 +215,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 } @@ -227,8 +227,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")) } diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index ee980333b72ff..df3b0a383a7d1 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -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.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.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, } if err := w.UpdateEvent(); err != nil { @@ -567,19 +566,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.ParamsInt64(":id")) } diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go new file mode 100644 index 0000000000000..1cf7e256ab93f --- /dev/null +++ b/routers/web/user/setting/webhooks.go @@ -0,0 +1,49 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +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", + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index 9b814c3f54246..852c804ed50f4 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -439,6 +439,40 @@ 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) + m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) + m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost) + m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) + m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) + m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) + m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) + m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) + m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) + m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) + m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) + m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) + m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) + m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) + m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) + m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) + m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) + m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsUserWebhook"] = true + }) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes @@ -719,6 +753,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) @@ -733,7 +768,10 @@ func RegisterRoutes(m *web.Route) { m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - }, webhooksEnabled) + m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + }, webhooksEnabled, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsOrganizationWebhook"] = true + }) m.Group("/labels", func() { m.Get("", org.RetrieveLabels, org.Labels) @@ -859,7 +897,9 @@ func RegisterRoutes(m *web.Route) { m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) - }, webhooksEnabled) + }, webhooksEnabled, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsRepositoryWebhook"] = true + }) m.Group("/keys", func() { m.Combo("").Get(repo.DeployKeys). diff --git a/services/repository/hooks.go b/services/repository/hooks.go index 45a031f38e6b0..58725bf019979 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -104,7 +104,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, }) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index e877e16edaa3f..bcdb27c7deb65 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -227,16 +227,16 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_mode owner = source.Repository.MustOwner() } - // 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: %v", err) } - ws = append(ws, orgHooks...) + ws = append(ws, ownerHooks...) } // Add any admin-defined system webhooks diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 767d66114c6ae..5cf406d1e5774 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -40,7 +40,7 @@ N/A {{end}} - {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin}} + {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin $.PageIsUserSettings}}