mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	System-wide webhooks (#10546)
* Create system webhook column (and migration)
* Create system webhook DB methods
Based on the default webhook ones
* Modify router to handle system webhooks and default ones
* Remove old unused admin nav template
* Adjust orgRepoCtx to differentiate system and default webhook URLs
* Assign IsSystemWebhook when creating webhooks
* Correctly use booleans for IsSystemWebhook
* Use system webhooks when preparing webhooks for payload
* Add UI and locale changes
* Use router params to differentiate admin hook pages
* Fix deleting admin webhooks and rename method
* Add clarity to webhook docs
* Revert "Remove old unused admin nav template"
This reverts commit 191a20a738.
* Rename WebHooksNewPost to GiteaHooksNewPost for clarity
* Reintroduce blank line lost during merge conflict
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
			
			
This commit is contained in:
		| @@ -15,24 +15,24 @@ menu: | ||||
|  | ||||
| # Webhooks | ||||
|  | ||||
| Gitea supports web hooks for repository events. This can be found in the settings | ||||
| page `/:username/:reponame/settings/hooks`. All event pushes are POST requests. | ||||
| The methods currently supported are: | ||||
| Gitea supports web hooks for repository events. This can be configured in the settings | ||||
| page `/:username/:reponame/settings/hooks` by a repository admin. Webhooks can also be configured on a per-organization and whole system basis. | ||||
| All event pushes are POST requests. The methods currently supported are: | ||||
|  | ||||
| - Gitea | ||||
| - Gitea (can also be a GET request) | ||||
| - Gogs | ||||
| - Slack | ||||
| - Discord | ||||
| - Dingtalk | ||||
| - Telegram | ||||
| - Microsoft Teams | ||||
| - Feishu | ||||
|  | ||||
| ### Event information | ||||
|  | ||||
| The following is an example of event information that will be sent by Gitea to | ||||
| a Payload URL: | ||||
|  | ||||
|  | ||||
| ``` | ||||
| X-GitHub-Delivery: f6266f16-1bf3-46a5-9ea4-602e06ead473 | ||||
| X-GitHub-Event: push | ||||
|   | ||||
| @@ -194,6 +194,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("remove dependencies from deleted repositories", purgeUnusedDependencies), | ||||
| 	// v130 -> v131 | ||||
| 	NewMigration("Expand webhooks for more granularity", expandWebhooks), | ||||
| 	// v131 -> v132 | ||||
| 	NewMigration("Add IsSystemWebhook column to webhooks table", addSystemWebhookColumn), | ||||
| } | ||||
|  | ||||
| // Migrate database to current version | ||||
|   | ||||
							
								
								
									
										22
									
								
								models/migrations/v131.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								models/migrations/v131.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // Copyright 2020 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 ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| func addSystemWebhookColumn(x *xorm.Engine) error { | ||||
| 	type Webhook struct { | ||||
| 		IsSystemWebhook bool `xorm:"NOT NULL DEFAULT false"` | ||||
| 	} | ||||
|  | ||||
| 	if err := x.Sync2(new(Webhook)); err != nil { | ||||
| 		return fmt.Errorf("Sync2: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -100,8 +100,9 @@ const ( | ||||
| // Webhook represents a web hook object. | ||||
| type Webhook struct { | ||||
| 	ID              int64 `xorm:"pk autoincr"` | ||||
| 	RepoID       int64  `xorm:"INDEX"` | ||||
| 	RepoID          int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook | ||||
| 	OrgID           int64 `xorm:"INDEX"` | ||||
| 	IsSystemWebhook bool | ||||
| 	URL             string `xorm:"url TEXT"` | ||||
| 	Signature       string `xorm:"TEXT"` | ||||
| 	HTTPMethod      string `xorm:"http_method"` | ||||
| @@ -401,7 +402,7 @@ func GetWebhooksByOrgID(orgID int64, listOptions ListOptions) ([]*Webhook, error | ||||
| func GetDefaultWebhook(id int64) (*Webhook, error) { | ||||
| 	webhook := &Webhook{ID: id} | ||||
| 	has, err := x. | ||||
| 		Where("repo_id=? AND org_id=?", 0, 0). | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false). | ||||
| 		Get(webhook) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -419,7 +420,33 @@ func GetDefaultWebhooks() ([]*Webhook, error) { | ||||
| func getDefaultWebhooks(e Engine) ([]*Webhook, error) { | ||||
| 	webhooks := make([]*Webhook, 0, 5) | ||||
| 	return webhooks, e. | ||||
| 		Where("repo_id=? AND org_id=?", 0, 0). | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false). | ||||
| 		Find(&webhooks) | ||||
| } | ||||
|  | ||||
| // GetSystemWebhook returns admin system webhook by given ID. | ||||
| func GetSystemWebhook(id int64) (*Webhook, error) { | ||||
| 	webhook := &Webhook{ID: id} | ||||
| 	has, err := x. | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true). | ||||
| 		Get(webhook) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrWebhookNotExist{id} | ||||
| 	} | ||||
| 	return webhook, nil | ||||
| } | ||||
|  | ||||
| // GetSystemWebhooks returns all admin system webhooks. | ||||
| func GetSystemWebhooks() ([]*Webhook, error) { | ||||
| 	return getSystemWebhooks(x) | ||||
| } | ||||
|  | ||||
| func getSystemWebhooks(e Engine) ([]*Webhook, error) { | ||||
| 	webhooks := make([]*Webhook, 0, 5) | ||||
| 	return webhooks, e. | ||||
| 		Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true). | ||||
| 		Find(&webhooks) | ||||
| } | ||||
|  | ||||
| @@ -471,8 +498,8 @@ func DeleteWebhookByOrgID(orgID, id int64) error { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // DeleteDefaultWebhook deletes an admin-default webhook by given ID. | ||||
| func DeleteDefaultWebhook(id int64) error { | ||||
| // DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0) | ||||
| func DeleteDefaultSystemWebhook(id int64) error { | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 	if err := sess.Begin(); err != nil { | ||||
|   | ||||
| @@ -181,6 +181,13 @@ func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api. | ||||
| 		ws = append(ws, orgHooks...) | ||||
| 	} | ||||
|  | ||||
| 	// Add any admin-defined system webhooks | ||||
| 	systemHooks, err := models.GetSystemWebhooks() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("GetSystemWebhooks: %v", err) | ||||
| 	} | ||||
| 	ws = append(ws, systemHooks...) | ||||
|  | ||||
| 	if len(ws) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|   | ||||
| @@ -1753,6 +1753,7 @@ users = User Accounts | ||||
| organizations = Organizations | ||||
| repositories = Repositories | ||||
| hooks = Default Webhooks | ||||
| systemhooks = System Webhooks | ||||
| authentication = Authentication Sources | ||||
| emails = User Emails | ||||
| config = Configuration | ||||
| @@ -1889,6 +1890,10 @@ hooks.desc = Webhooks automatically make HTTP POST requests to a server when cer | ||||
| hooks.add_webhook = Add Default Webhook | ||||
| hooks.update_webhook = Update Default Webhook | ||||
|  | ||||
| systemhooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Webhooks defined will act on all repositories on the system, so please consider any performance implications this may have. Read more in the <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">webhooks guide</a>. | ||||
| systemhooks.add_webhook = Add System Webhook | ||||
| systemhooks.update_webhook = Update System Webhook | ||||
|  | ||||
| auths.auth_manage_panel = Authentication Source Management | ||||
| auths.new = Add Authentication Source | ||||
| auths.name = Name | ||||
|   | ||||
| @@ -12,20 +12,32 @@ import ( | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// tplAdminHooks template path for render hook settings | ||||
| 	// tplAdminHooks template path to render hook settings | ||||
| 	tplAdminHooks base.TplName = "admin/hooks" | ||||
| ) | ||||
|  | ||||
| // DefaultWebhooks render admin-default webhook list page | ||||
| func DefaultWebhooks(ctx *context.Context) { | ||||
| // DefaultOrSystemWebhooks renders both admin default and system webhook list pages | ||||
| func DefaultOrSystemWebhooks(ctx *context.Context) { | ||||
| 	var ws []*models.Webhook | ||||
| 	var err error | ||||
|  | ||||
| 	// Are we looking at default webhooks? | ||||
| 	if ctx.Params(":configType") == "hooks" { | ||||
| 		ctx.Data["Title"] = ctx.Tr("admin.hooks") | ||||
| 		ctx.Data["Description"] = ctx.Tr("admin.hooks.desc") | ||||
| 		ctx.Data["PageIsAdminHooks"] = true | ||||
| 		ctx.Data["BaseLink"] = setting.AppSubURL + "/admin/hooks" | ||||
| 	ctx.Data["Description"] = ctx.Tr("admin.hooks.desc") | ||||
| 		ws, err = models.GetDefaultWebhooks() | ||||
| 	} else { | ||||
| 		ctx.Data["Title"] = ctx.Tr("admin.systemhooks") | ||||
| 		ctx.Data["Description"] = ctx.Tr("admin.systemhooks.desc") | ||||
| 		ctx.Data["PageIsAdminSystemHooks"] = true | ||||
| 		ctx.Data["BaseLink"] = setting.AppSubURL + "/admin/system-hooks" | ||||
| 		ws, err = models.GetSystemWebhooks() | ||||
| 	} | ||||
|  | ||||
| 	ws, err := models.GetDefaultWebhooks() | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetWebhooksDefaults", err) | ||||
| 		ctx.ServerError("GetWebhooksAdmin", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -33,15 +45,22 @@ func DefaultWebhooks(ctx *context.Context) { | ||||
| 	ctx.HTML(200, tplAdminHooks) | ||||
| } | ||||
|  | ||||
| // DeleteDefaultWebhook response for delete admin-default webhook | ||||
| func DeleteDefaultWebhook(ctx *context.Context) { | ||||
| 	if err := models.DeleteDefaultWebhook(ctx.QueryInt64("id")); err != nil { | ||||
| // DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook | ||||
| func DeleteDefaultOrSystemWebhook(ctx *context.Context) { | ||||
| 	if err := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
| 	} | ||||
|  | ||||
| 	// Are we looking at default webhooks? | ||||
| 	if ctx.Params(":configType") == "hooks" { | ||||
| 		ctx.JSON(200, map[string]interface{}{ | ||||
| 			"redirect": setting.AppSubURL + "/admin/hooks", | ||||
| 		}) | ||||
| 	} else { | ||||
| 		ctx.JSON(200, map[string]interface{}{ | ||||
| 			"redirect": setting.AppSubURL + "/admin/system-hooks", | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -52,11 +52,12 @@ type orgRepoCtx struct { | ||||
| 	OrgID           int64 | ||||
| 	RepoID          int64 | ||||
| 	IsAdmin         bool | ||||
| 	IsSystemWebhook bool | ||||
| 	Link            string | ||||
| 	NewTemplate     base.TplName | ||||
| } | ||||
|  | ||||
| // getOrgRepoCtx determines whether this is a repo, organization, or admin context. | ||||
| // 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{ | ||||
| @@ -75,6 +76,8 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { | ||||
| 	} | ||||
|  | ||||
| 	if ctx.User.IsAdmin { | ||||
| 		// Are we looking at default webhooks? | ||||
| 		if ctx.Params(":configType") == "hooks" { | ||||
| 			return &orgRepoCtx{ | ||||
| 				IsAdmin:     true, | ||||
| 				Link:        path.Join(setting.AppSubURL, "/admin/hooks"), | ||||
| @@ -82,6 +85,15 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { | ||||
| 			}, nil | ||||
| 		} | ||||
|  | ||||
| 		// Must be system webhooks instead | ||||
| 		return &orgRepoCtx{ | ||||
| 			IsAdmin:         true, | ||||
| 			IsSystemWebhook: true, | ||||
| 			Link:            path.Join(setting.AppSubURL, "/admin/system-hooks"), | ||||
| 			NewTemplate:     tplAdminHookNew, | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, errors.New("Unable to set OrgRepo context") | ||||
| } | ||||
|  | ||||
| @@ -105,7 +117,10 @@ func WebhooksNew(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if orCtx.IsAdmin { | ||||
| 	if orCtx.IsAdmin && orCtx.IsSystemWebhook { | ||||
| 		ctx.Data["PageIsAdminSystemHooks"] = true | ||||
| 		ctx.Data["PageIsAdminSystemHooksNew"] = true | ||||
| 	} else if orCtx.IsAdmin { | ||||
| 		ctx.Data["PageIsAdminHooks"] = true | ||||
| 		ctx.Data["PageIsAdminHooksNew"] = true | ||||
| 	} else { | ||||
| @@ -159,8 +174,8 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WebHooksNewPost response for creating webhook | ||||
| func WebHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) { | ||||
| // GiteaHooksNewPost response for creating Gitea webhook | ||||
| func GiteaHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) { | ||||
| 	ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") | ||||
| 	ctx.Data["PageIsSettingsHooks"] = true | ||||
| 	ctx.Data["PageIsSettingsHooksNew"] = true | ||||
| @@ -194,6 +209,7 @@ func WebHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) { | ||||
| 		IsActive:        form.Active, | ||||
| 		HookTaskType:    models.GITEA, | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -246,6 +262,7 @@ func newGogsWebhookPost(ctx *context.Context, form auth.NewGogshookForm, kind mo | ||||
| 		IsActive:        form.Active, | ||||
| 		HookTaskType:    kind, | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -295,6 +312,7 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) { | ||||
| 		HookTaskType:    models.DISCORD, | ||||
| 		Meta:            string(meta), | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -335,6 +353,7 @@ func DingtalkHooksNewPost(ctx *context.Context, form auth.NewDingtalkHookForm) { | ||||
| 		HookTaskType:    models.DINGTALK, | ||||
| 		Meta:            "", | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -384,6 +403,7 @@ func TelegramHooksNewPost(ctx *context.Context, form auth.NewTelegramHookForm) { | ||||
| 		HookTaskType:    models.TELEGRAM, | ||||
| 		Meta:            string(meta), | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -424,6 +444,7 @@ func MSTeamsHooksNewPost(ctx *context.Context, form auth.NewMSTeamsHookForm) { | ||||
| 		HookTaskType:    models.MSTEAMS, | ||||
| 		Meta:            "", | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -481,6 +502,7 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) { | ||||
| 		HookTaskType:    models.SLACK, | ||||
| 		Meta:            string(meta), | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -521,6 +543,7 @@ func FeishuHooksNewPost(ctx *context.Context, form auth.NewFeishuHookForm) { | ||||
| 		HookTaskType:    models.FEISHU, | ||||
| 		Meta:            "", | ||||
| 		OrgID:           orCtx.OrgID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	if err := w.UpdateEvent(); err != nil { | ||||
| 		ctx.ServerError("UpdateEvent", err) | ||||
| @@ -549,6 +572,8 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) { | ||||
| 		w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.OrgID > 0 { | ||||
| 		w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.IsSystemWebhook { | ||||
| 		w, err = models.GetSystemWebhook(ctx.ParamsInt64(":id")) | ||||
| 	} else { | ||||
| 		w, err = models.GetDefaultWebhook(ctx.ParamsInt64(":id")) | ||||
| 	} | ||||
|   | ||||
| @@ -458,11 +458,11 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||
| 			m.Post("/delete", admin.DeleteRepo) | ||||
| 		}) | ||||
|  | ||||
| 		m.Group("/hooks", func() { | ||||
| 			m.Get("", admin.DefaultWebhooks) | ||||
| 			m.Post("/delete", admin.DeleteDefaultWebhook) | ||||
| 		m.Group("/^:configType(hooks|system-hooks)$", func() { | ||||
| 			m.Get("", admin.DefaultOrSystemWebhooks) | ||||
| 			m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) | ||||
| 			m.Get("/:type/new", repo.WebhooksNew) | ||||
| 			m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) | ||||
| 			m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 			m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 			m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 			m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| @@ -569,7 +569,7 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||
| 					m.Get("", org.Webhooks) | ||||
| 					m.Post("/delete", org.DeleteWebhook) | ||||
| 					m.Get("/:type/new", repo.WebhooksNew) | ||||
| 					m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) | ||||
| 					m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 					m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 					m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 					m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
| @@ -635,7 +635,7 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||
| 				m.Get("", repo.Webhooks) | ||||
| 				m.Post("/delete", repo.DeleteWebhook) | ||||
| 				m.Get("/:type/new", repo.WebhooksNew) | ||||
| 				m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksNewPost) | ||||
| 				m.Post("/gitea/new", bindIgnErr(auth.NewWebhookForm{}), repo.GiteaHooksNewPost) | ||||
| 				m.Post("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) | ||||
| 				m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) | ||||
| 				m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) | ||||
|   | ||||
| @@ -14,6 +14,9 @@ | ||||
| 	<a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks"> | ||||
| 		{{.i18n.Tr "admin.hooks"}} | ||||
| 	</a> | ||||
| 	<a class="{{if .PageIsAdminSystemHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/system-hooks"> | ||||
| 		{{.i18n.Tr "admin.systemhooks"}} | ||||
| 	</a> | ||||
| 	<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths"> | ||||
| 		{{.i18n.Tr "admin.authentication"}} | ||||
| 	</a> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 James Lakin
					James Lakin