2023-09-13 19:24:06 +00:00
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
2023-10-13 16:10:37 +00:00
"bytes"
2023-10-17 18:40:09 +00:00
"context"
2023-10-13 16:10:37 +00:00
"crypto/hmac"
"crypto/sha256"
2023-10-17 17:42:00 +00:00
"encoding/base64"
2023-10-13 16:10:37 +00:00
"encoding/hex"
"encoding/json"
"net/http"
2023-10-17 17:42:00 +00:00
"net/url"
2023-09-14 10:21:20 +00:00
"sort"
2023-10-20 10:42:28 +00:00
"strings"
2023-09-13 19:25:05 +00:00
"sync"
2023-09-13 19:24:06 +00:00
"time"
2023-10-17 18:35:05 +00:00
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/version"
"code.vikunja.io/web"
2023-09-13 19:24:06 +00:00
"xorm.io/xorm"
)
2023-10-17 17:50:45 +00:00
var webhookClient * http . Client
2023-09-13 19:24:06 +00:00
type Webhook struct {
2023-10-17 18:29:42 +00:00
// The generated ID of this webhook target
ID int64 ` xorm:"bigint autoincr not null unique pk" json:"id" param:"webhook" `
// The target URL where the POST request with the webhook payload will be made
2023-10-20 10:42:28 +00:00
TargetURL string ` xorm:"not null" valid:"required,url" json:"target_url" `
2023-10-17 18:29:42 +00:00
// The webhook events which should fire this webhook target
2023-10-20 10:42:28 +00:00
Events [ ] string ` xorm:"JSON not null" valid:"required" json:"events" `
2023-10-17 18:29:42 +00:00
// The project ID of the project this webhook target belongs to
ProjectID int64 ` xorm:"bigint not null index" json:"project_id" param:"project" `
// If provided, webhook requests will be signed using HMAC. Check out the docs about how to use this: https://vikunja.io/docs/webhooks/#signing
Secret string ` xorm:"null" json:"secret" `
2023-09-13 19:24:06 +00:00
// The user who initially created the webhook target.
CreatedBy * user . User ` xorm:"-" json:"created_by" valid:"-" `
CreatedByID int64 ` xorm:"bigint not null" json:"-" `
// A timestamp when this webhook target was created. You cannot change this value.
Created time . Time ` xorm:"created not null" json:"created" `
// A timestamp when this webhook target was last updated. You cannot change this value.
Updated time . Time ` xorm:"updated not null" json:"updated" `
web . CRUDable ` xorm:"-" json:"-" `
web . Rights ` xorm:"-" json:"-" `
}
func ( w * Webhook ) TableName ( ) string {
return "webhooks"
}
2023-09-13 19:25:05 +00:00
var availableWebhookEvents map [ string ] bool
var availableWebhookEventsLock * sync . Mutex
func init ( ) {
availableWebhookEvents = make ( map [ string ] bool )
availableWebhookEventsLock = & sync . Mutex { }
}
2023-09-14 10:22:57 +00:00
func RegisterEventForWebhook ( event events . Event ) {
2023-09-13 19:25:05 +00:00
availableWebhookEventsLock . Lock ( )
defer availableWebhookEventsLock . Unlock ( )
availableWebhookEvents [ event . Name ( ) ] = true
2023-09-13 19:36:00 +00:00
events . RegisterListener ( event . Name ( ) , & WebhookListener {
EventName : event . Name ( ) ,
} )
2023-09-13 19:25:05 +00:00
}
2023-09-14 10:21:20 +00:00
func GetAvailableWebhookEvents ( ) [ ] string {
evts := [ ] string { }
for e := range availableWebhookEvents {
evts = append ( evts , e )
}
sort . Strings ( evts )
return evts
}
2023-10-17 18:29:42 +00:00
// Create creates a webhook target
// @Summary Create a webhook target
2023-10-17 18:35:05 +00:00
// @Description Create a webhook target which receives POST requests about specified events from a project.
2023-10-17 18:29:42 +00:00
// @tags webhooks
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Project ID"
// @Param webhook body models.Webhook true "The webhook target object with required fields"
// @Success 200 {object} models.Webhook "The created webhook target."
// @Failure 400 {object} web.HTTPError "Invalid webhook object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/webhooks [put]
2023-09-13 19:24:06 +00:00
func ( w * Webhook ) Create ( s * xorm . Session , a web . Auth ) ( err error ) {
2023-10-20 10:42:28 +00:00
if ! strings . HasPrefix ( w . TargetURL , "http" ) {
return InvalidFieldError ( [ ] string { "target_url" } )
}
for _ , event := range w . Events {
if _ , has := availableWebhookEvents [ event ] ; ! has {
return InvalidFieldError ( [ ] string { "events" } )
}
}
2023-09-13 19:24:06 +00:00
w . CreatedByID = a . GetID ( )
_ , err = s . Insert ( w )
2023-10-18 20:18:45 +00:00
if err != nil {
return err
}
w . CreatedBy , err = user . GetUserByID ( s , a . GetID ( ) )
2023-09-13 19:24:06 +00:00
return
}
2023-10-17 18:29:42 +00:00
// ReadAll returns all webhook targets for a project
// @Summary Get all api webhook targets for the specified project
// @Description Get all api webhook targets for the specified project.
// @tags webhooks
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per bucket per page. This parameter is limited by the configured maximum of items per page."
// @Param id path int true "Project ID"
// @Success 200 {array} models.Webhook "The list of all webhook targets"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /projects/{id}/webhooks [get]
2023-10-17 18:40:09 +00:00
func ( w * Webhook ) ReadAll ( s * xorm . Session , a web . Auth , _ string , page int , perPage int ) ( result interface { } , resultCount int , numberOfTotalItems int64 , err error ) {
2023-09-13 19:24:06 +00:00
p := & Project { ID : w . ProjectID }
can , _ , err := p . CanRead ( s , a )
if err != nil {
return nil , 0 , 0 , err
}
if ! can {
return nil , 0 , 0 , ErrGenericForbidden { }
}
ws := [ ] * Webhook { }
err = s . Where ( "project_id = ?" , w . ProjectID ) .
Limit ( getLimitFromPageIndex ( page , perPage ) ) .
Find ( & ws )
if err != nil {
return
}
total , err := s . Where ( "project_id = ?" , w . ProjectID ) .
Count ( & Webhook { } )
if err != nil {
return
}
2023-10-18 18:06:07 +00:00
userIDs := [ ] int64 { }
for _ , webhook := range ws {
userIDs = append ( userIDs , webhook . CreatedByID )
}
users , err := user . GetUsersByIDs ( s , userIDs )
if err != nil {
return nil , 0 , 0 , err
}
for _ , webhook := range ws {
webhook . Secret = ""
webhook . CreatedBy = users [ webhook . CreatedByID ]
}
2023-09-13 19:24:06 +00:00
return ws , len ( ws ) , total , err
}
2023-10-17 18:29:42 +00:00
// Update updates a webhook target
// @Summary Change a webhook target's events.
// @Description Change a webhook target's events. You cannot change other values of a webhook.
// @tags webhooks
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Project ID"
// @Param webhookID path int true "Webhook ID"
// @Success 200 {object} models.Webhook "Updated webhook target"
// @Failure 404 {object} web.HTTPError "The webhok target does not exist"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/webhooks/{webhookID} [post]
2023-10-17 18:40:09 +00:00
func ( w * Webhook ) Update ( s * xorm . Session , _ web . Auth ) ( err error ) {
2023-10-20 10:42:28 +00:00
for _ , event := range w . Events {
if _ , has := availableWebhookEvents [ event ] ; ! has {
return InvalidFieldError ( [ ] string { "events" } )
}
}
2023-09-13 19:24:06 +00:00
_ , err = s . Where ( "id = ?" , w . ID ) .
Cols ( "events" ) .
Update ( w )
return
}
2023-10-17 18:29:42 +00:00
// Delete deletes a webhook target
// @Summary Deletes an existing webhook target
// @Description Delete any of the project's webhook targets.
// @tags webhooks
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Project ID"
// @Param webhookID path int true "Webhook ID"
// @Success 200 {object} models.Message "Successfully deleted."
// @Failure 404 {object} web.HTTPError "The webhok target does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/webhooks/{webhookID} [delete]
2023-10-17 18:40:09 +00:00
func ( w * Webhook ) Delete ( s * xorm . Session , _ web . Auth ) ( err error ) {
2023-09-13 19:24:06 +00:00
_ , err = s . Where ( "id = ?" , w . ID ) . Delete ( & Webhook { } )
return
}
2023-10-13 16:10:37 +00:00
2023-10-17 17:42:00 +00:00
func getWebhookHTTPClient ( ) ( client * http . Client ) {
2023-10-17 17:50:45 +00:00
if webhookClient != nil {
return webhookClient
}
2023-10-17 17:42:00 +00:00
client = http . DefaultClient
client . Timeout = time . Duration ( config . WebhooksTimeoutSeconds . GetInt ( ) ) * time . Second
if config . WebhooksProxyURL . GetString ( ) == "" || config . WebhooksProxyPassword . GetString ( ) == "" {
2023-10-17 17:50:45 +00:00
webhookClient = client
2023-10-17 17:42:00 +00:00
return
}
proxyURL , _ := url . Parse ( config . WebhooksProxyURL . GetString ( ) )
client . Transport = & http . Transport {
Proxy : http . ProxyURL ( proxyURL ) ,
ProxyConnectHeader : http . Header {
"Proxy-Authorization" : [ ] string { "Basic " + base64 . StdEncoding . EncodeToString ( [ ] byte ( "vikunja:" + config . WebhooksProxyPassword . GetString ( ) ) ) } ,
"User-Agent" : [ ] string { "Vikunja/" + version . Version } ,
} ,
}
2023-10-17 17:50:45 +00:00
webhookClient = client
2023-10-17 17:42:00 +00:00
return
}
2023-10-13 16:10:37 +00:00
func ( w * Webhook ) sendWebhookPayload ( p * WebhookPayload ) ( err error ) {
payload , err := json . Marshal ( p )
if err != nil {
return err
}
2023-10-17 18:40:09 +00:00
req , err := http . NewRequestWithContext ( context . Background ( ) , http . MethodPost , w . TargetURL , bytes . NewReader ( payload ) )
2023-10-13 16:10:37 +00:00
if err != nil {
return err
}
if len ( w . Secret ) > 0 {
sig256 := hmac . New ( sha256 . New , [ ] byte ( w . Secret ) )
_ , err = sig256 . Write ( payload )
if err != nil {
log . Errorf ( "Could not generate webhook signature for Webhook %d: %s" , w . ID , err )
}
signature := hex . EncodeToString ( sig256 . Sum ( nil ) )
req . Header . Add ( "X-Vikunja-Signature" , signature )
}
req . Header . Add ( "User-Agent" , "Vikunja/" + version . Version )
2023-12-06 13:09:49 +00:00
req . Header . Add ( "Content-Type" , "application/json" )
2023-10-13 16:10:37 +00:00
2023-10-17 17:42:00 +00:00
client := getWebhookHTTPClient ( )
2023-10-17 18:40:09 +00:00
res , err := client . Do ( req )
if err != nil {
return err
2023-10-13 16:10:37 +00:00
}
2023-10-17 18:40:09 +00:00
defer res . Body . Close ( )
log . Debugf ( "Sent webhook payload for webhook %d for event %s" , w . ID , p . EventName )
2023-10-13 16:10:37 +00:00
return
}