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:
James Lakin 2020-03-08 22:08:05 +00:00 committed by GitHub
parent b8551f8532
commit a9f4489bbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 232 additions and 122 deletions

View file

@ -15,24 +15,24 @@ menu:
# Webhooks # Webhooks
Gitea supports web hooks for repository events. This can be found in the settings Gitea supports web hooks for repository events. This can be configured in the settings
page `/:username/:reponame/settings/hooks`. All event pushes are POST requests. page `/:username/:reponame/settings/hooks` by a repository admin. Webhooks can also be configured on a per-organization and whole system basis.
The methods currently supported are: All event pushes are POST requests. The methods currently supported are:
- Gitea - Gitea (can also be a GET request)
- Gogs - Gogs
- Slack - Slack
- Discord - Discord
- Dingtalk - Dingtalk
- Telegram - Telegram
- Microsoft Teams - Microsoft Teams
- Feishu
### Event information ### Event information
The following is an example of event information that will be sent by Gitea to The following is an example of event information that will be sent by Gitea to
a Payload URL: a Payload URL:
``` ```
X-GitHub-Delivery: f6266f16-1bf3-46a5-9ea4-602e06ead473 X-GitHub-Delivery: f6266f16-1bf3-46a5-9ea4-602e06ead473
X-GitHub-Event: push X-GitHub-Event: push

View file

@ -194,6 +194,8 @@ var migrations = []Migration{
NewMigration("remove dependencies from deleted repositories", purgeUnusedDependencies), NewMigration("remove dependencies from deleted repositories", purgeUnusedDependencies),
// v130 -> v131 // v130 -> v131
NewMigration("Expand webhooks for more granularity", expandWebhooks), NewMigration("Expand webhooks for more granularity", expandWebhooks),
// v131 -> v132
NewMigration("Add IsSystemWebhook column to webhooks table", addSystemWebhookColumn),
} }
// Migrate database to current version // Migrate database to current version

22
models/migrations/v131.go Normal file
View 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
}

View file

@ -100,8 +100,9 @@ const (
// Webhook represents a web hook object. // Webhook represents a web hook object.
type Webhook struct { type Webhook struct {
ID int64 `xorm:"pk autoincr"` 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"` OrgID int64 `xorm:"INDEX"`
IsSystemWebhook bool
URL string `xorm:"url TEXT"` URL string `xorm:"url TEXT"`
Signature string `xorm:"TEXT"` Signature string `xorm:"TEXT"`
HTTPMethod string `xorm:"http_method"` HTTPMethod string `xorm:"http_method"`
@ -401,7 +402,7 @@ func GetWebhooksByOrgID(orgID int64, listOptions ListOptions) ([]*Webhook, error
func GetDefaultWebhook(id int64) (*Webhook, error) { func GetDefaultWebhook(id int64) (*Webhook, error) {
webhook := &Webhook{ID: id} webhook := &Webhook{ID: id}
has, err := x. 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) Get(webhook)
if err != nil { if err != nil {
return nil, err return nil, err
@ -419,7 +420,33 @@ func GetDefaultWebhooks() ([]*Webhook, error) {
func getDefaultWebhooks(e Engine) ([]*Webhook, error) { func getDefaultWebhooks(e Engine) ([]*Webhook, error) {
webhooks := make([]*Webhook, 0, 5) webhooks := make([]*Webhook, 0, 5)
return webhooks, e. 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) Find(&webhooks)
} }
@ -471,8 +498,8 @@ func DeleteWebhookByOrgID(orgID, id int64) error {
}) })
} }
// DeleteDefaultWebhook deletes an admin-default webhook by given ID. // DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
func DeleteDefaultWebhook(id int64) error { func DeleteDefaultSystemWebhook(id int64) error {
sess := x.NewSession() sess := x.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {

View file

@ -181,6 +181,13 @@ func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.
ws = append(ws, orgHooks...) 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 { if len(ws) == 0 {
return nil return nil
} }

View file

@ -1753,6 +1753,7 @@ users = User Accounts
organizations = Organizations organizations = Organizations
repositories = Repositories repositories = Repositories
hooks = Default Webhooks hooks = Default Webhooks
systemhooks = System Webhooks
authentication = Authentication Sources authentication = Authentication Sources
emails = User Emails emails = User Emails
config = Configuration 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.add_webhook = Add Default Webhook
hooks.update_webhook = Update 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.auth_manage_panel = Authentication Source Management
auths.new = Add Authentication Source auths.new = Add Authentication Source
auths.name = Name auths.name = Name

View file

@ -12,20 +12,32 @@ import (
) )
const ( const (
// tplAdminHooks template path for render hook settings // tplAdminHooks template path to render hook settings
tplAdminHooks base.TplName = "admin/hooks" tplAdminHooks base.TplName = "admin/hooks"
) )
// DefaultWebhooks render admin-default webhook list page // DefaultOrSystemWebhooks renders both admin default and system webhook list pages
func DefaultWebhooks(ctx *context.Context) { 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["Title"] = ctx.Tr("admin.hooks")
ctx.Data["Description"] = ctx.Tr("admin.hooks.desc")
ctx.Data["PageIsAdminHooks"] = true ctx.Data["PageIsAdminHooks"] = true
ctx.Data["BaseLink"] = setting.AppSubURL + "/admin/hooks" 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 { if err != nil {
ctx.ServerError("GetWebhooksDefaults", err) ctx.ServerError("GetWebhooksAdmin", err)
return return
} }
@ -33,15 +45,22 @@ func DefaultWebhooks(ctx *context.Context) {
ctx.HTML(200, tplAdminHooks) ctx.HTML(200, tplAdminHooks)
} }
// DeleteDefaultWebhook response for delete admin-default webhook // DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
func DeleteDefaultWebhook(ctx *context.Context) { func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
if err := models.DeleteDefaultWebhook(ctx.QueryInt64("id")); err != nil { if err := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error()) ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
} else { } else {
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) 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{}{ ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/hooks", "redirect": setting.AppSubURL + "/admin/hooks",
}) })
} else {
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/system-hooks",
})
}
} }

View file

@ -52,11 +52,12 @@ type orgRepoCtx struct {
OrgID int64 OrgID int64
RepoID int64 RepoID int64
IsAdmin bool IsAdmin bool
IsSystemWebhook bool
Link string Link string
NewTemplate base.TplName 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) { func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
if len(ctx.Repo.RepoLink) > 0 { if len(ctx.Repo.RepoLink) > 0 {
return &orgRepoCtx{ return &orgRepoCtx{
@ -75,6 +76,8 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
} }
if ctx.User.IsAdmin { if ctx.User.IsAdmin {
// Are we looking at default webhooks?
if ctx.Params(":configType") == "hooks" {
return &orgRepoCtx{ return &orgRepoCtx{
IsAdmin: true, IsAdmin: true,
Link: path.Join(setting.AppSubURL, "/admin/hooks"), Link: path.Join(setting.AppSubURL, "/admin/hooks"),
@ -82,6 +85,15 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
}, nil }, 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") return nil, errors.New("Unable to set OrgRepo context")
} }
@ -105,7 +117,10 @@ func WebhooksNew(ctx *context.Context) {
return 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["PageIsAdminHooks"] = true
ctx.Data["PageIsAdminHooksNew"] = true ctx.Data["PageIsAdminHooksNew"] = true
} else { } else {
@ -159,8 +174,8 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
} }
} }
// WebHooksNewPost response for creating webhook // GiteaHooksNewPost response for creating Gitea webhook
func WebHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) { func GiteaHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["PageIsSettingsHooks"] = true ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["PageIsSettingsHooksNew"] = true ctx.Data["PageIsSettingsHooksNew"] = true
@ -194,6 +209,7 @@ func WebHooksNewPost(ctx *context.Context, form auth.NewWebhookForm) {
IsActive: form.Active, IsActive: form.Active,
HookTaskType: models.GITEA, HookTaskType: models.GITEA,
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -246,6 +262,7 @@ func newGogsWebhookPost(ctx *context.Context, form auth.NewGogshookForm, kind mo
IsActive: form.Active, IsActive: form.Active,
HookTaskType: kind, HookTaskType: kind,
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -295,6 +312,7 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) {
HookTaskType: models.DISCORD, HookTaskType: models.DISCORD,
Meta: string(meta), Meta: string(meta),
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -335,6 +353,7 @@ func DingtalkHooksNewPost(ctx *context.Context, form auth.NewDingtalkHookForm) {
HookTaskType: models.DINGTALK, HookTaskType: models.DINGTALK,
Meta: "", Meta: "",
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -384,6 +403,7 @@ func TelegramHooksNewPost(ctx *context.Context, form auth.NewTelegramHookForm) {
HookTaskType: models.TELEGRAM, HookTaskType: models.TELEGRAM,
Meta: string(meta), Meta: string(meta),
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -424,6 +444,7 @@ func MSTeamsHooksNewPost(ctx *context.Context, form auth.NewMSTeamsHookForm) {
HookTaskType: models.MSTEAMS, HookTaskType: models.MSTEAMS,
Meta: "", Meta: "",
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -481,6 +502,7 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
HookTaskType: models.SLACK, HookTaskType: models.SLACK,
Meta: string(meta), Meta: string(meta),
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
@ -521,6 +543,7 @@ func FeishuHooksNewPost(ctx *context.Context, form auth.NewFeishuHookForm) {
HookTaskType: models.FEISHU, HookTaskType: models.FEISHU,
Meta: "", Meta: "",
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook,
} }
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) 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")) w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
} else if orCtx.OrgID > 0 { } else if orCtx.OrgID > 0 {
w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
} else if orCtx.IsSystemWebhook {
w, err = models.GetSystemWebhook(ctx.ParamsInt64(":id"))
} else { } else {
w, err = models.GetDefaultWebhook(ctx.ParamsInt64(":id")) w, err = models.GetDefaultWebhook(ctx.ParamsInt64(":id"))
} }

View file

@ -458,11 +458,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/delete", admin.DeleteRepo) m.Post("/delete", admin.DeleteRepo)
}) })
m.Group("/hooks", func() { m.Group("/^:configType(hooks|system-hooks)$", func() {
m.Get("", admin.DefaultWebhooks) m.Get("", admin.DefaultOrSystemWebhooks)
m.Post("/delete", admin.DeleteDefaultWebhook) m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
m.Get("/:type/new", repo.WebhooksNew) 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("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
@ -569,7 +569,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", org.Webhooks) m.Get("", org.Webhooks)
m.Post("/delete", org.DeleteWebhook) m.Post("/delete", org.DeleteWebhook)
m.Get("/:type/new", repo.WebhooksNew) 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("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
@ -635,7 +635,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", repo.Webhooks) m.Get("", repo.Webhooks)
m.Post("/delete", repo.DeleteWebhook) m.Post("/delete", repo.DeleteWebhook)
m.Get("/:type/new", repo.WebhooksNew) 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("/gogs/new", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost)
m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost) m.Post("/slack/new", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksNewPost)
m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost) m.Post("/discord/new", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksNewPost)

View file

@ -14,6 +14,9 @@
<a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks"> <a class="{{if .PageIsAdminHooks}}active{{end}} item" href="{{AppSubUrl}}/admin/hooks">
{{.i18n.Tr "admin.hooks"}} {{.i18n.Tr "admin.hooks"}}
</a> </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"> <a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
{{.i18n.Tr "admin.authentication"}} {{.i18n.Tr "admin.authentication"}}
</a> </a>