diff --git a/pkg/routes/api/v1/user_webhooks.go b/pkg/routes/api/v1/user_webhooks.go new file mode 100644 index 000000000..ef715d159 --- /dev/null +++ b/pkg/routes/api/v1/user_webhooks.go @@ -0,0 +1,237 @@ +// 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 License 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 License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package v1 + +import ( + "net/http" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/user" + + "github.com/labstack/echo/v5" +) + +// GetUserWebhooks returns all webhook targets for the current user +// @Summary Get all user-level webhook targets +// @Description Get all webhook targets configured for the current user (not project-specific). +// @tags webhooks +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Success 200 {array} models.Webhook "The list of webhook targets" +// @Failure 500 {object} models.Message "Internal server error" +// @Router /user/settings/webhooks [get] +func GetUserWebhooks(c *echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return err + } + + s := db.NewSession() + defer s.Close() + + ws := []*models.Webhook{} + err = s.Where("user_id = ?", u.ID).Find(&ws) + if err != nil { + return err + } + + userIDs := []int64{} + for _, w := range ws { + userIDs = append(userIDs, w.CreatedByID) + } + + users, err := user.GetUsersByIDs(s, userIDs) + if err != nil { + return err + } + + for _, w := range ws { + w.Secret = "" + if createdBy, has := users[w.CreatedByID]; has { + w.CreatedBy = createdBy + } + } + + return c.JSON(http.StatusOK, ws) +} + +// CreateUserWebhook creates a new user-level webhook target +// @Summary Create a user-level webhook target +// @Description Create a webhook target for the current user that receives events across all projects. +// @tags webhooks +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param webhook body models.Webhook true "The webhook target" +// @Success 200 {object} models.Webhook "The created webhook target" +// @Failure 400 {object} web.HTTPError "Invalid webhook" +// @Failure 500 {object} models.Message "Internal server error" +// @Router /user/settings/webhooks [put] +func CreateUserWebhook(c *echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return err + } + + w := &models.Webhook{} + if err := c.Bind(w); err != nil { + return err + } + + // Force user-level webhook + w.UserID = u.ID + w.ProjectID = 0 + + s := db.NewSession() + defer s.Close() + + if err := s.Begin(); err != nil { + return err + } + + err = w.Create(s, u) + if err != nil { + _ = s.Rollback() + return err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + return c.JSON(http.StatusOK, w) +} + +// UpdateUserWebhook updates a user-level webhook target +// @Summary Update a user-level webhook target +// @Description Update the events for a user-level webhook target. +// @tags webhooks +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Webhook ID" +// @Success 200 {object} models.Webhook "The updated webhook target" +// @Failure 404 {object} web.HTTPError "Webhook not found" +// @Failure 500 {object} models.Message "Internal server error" +// @Router /user/settings/webhooks/{id} [post] +func UpdateUserWebhook(c *echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return err + } + + w := &models.Webhook{} + if err := c.Bind(w); err != nil { + return err + } + + s := db.NewSession() + defer s.Close() + + can, err := w.CanUpdate(s, u) + if err != nil { + return err + } + if !can { + return echo.ErrNotFound + } + + if err := s.Begin(); err != nil { + return err + } + + err = w.Update(s, u) + if err != nil { + _ = s.Rollback() + return err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + return c.JSON(http.StatusOK, w) +} + +// DeleteUserWebhook deletes a user-level webhook target +// @Summary Delete a user-level webhook target +// @Description Delete a user-level webhook target. +// @tags webhooks +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Webhook ID" +// @Success 200 {object} models.Message "Successfully deleted" +// @Failure 404 {object} web.HTTPError "Webhook not found" +// @Failure 500 {object} models.Message "Internal server error" +// @Router /user/settings/webhooks/{id} [delete] +func DeleteUserWebhook(c *echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return err + } + + w := &models.Webhook{} + if err := c.Bind(w); err != nil { + return err + } + + s := db.NewSession() + defer s.Close() + + can, err := w.CanDelete(s, u) + if err != nil { + return err + } + if !can { + return echo.ErrNotFound + } + + if err := s.Begin(); err != nil { + return err + } + + err = w.Delete(s, u) + if err != nil { + _ = s.Rollback() + return err + } + + if err := s.Commit(); err != nil { + _ = s.Rollback() + return err + } + + return c.JSON(http.StatusOK, &models.Message{Message: "Successfully deleted."}) +} + +// GetUserDirectedWebhookEvents returns events available for user-level webhooks +// @Summary Get available user-directed webhook events +// @Description Get all webhook events that can be used with user-level webhook targets. +// @tags webhooks +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Success 200 {array} string "The list of user-directed webhook events" +// @Router /user/settings/webhooks/events [get] +func GetUserDirectedWebhookEvents(c *echo.Context) error { + return c.JSON(http.StatusOK, models.GetUserDirectedWebhookEvents()) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index d47e61abe..be040f349 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -397,6 +397,15 @@ func registerAPIRoutes(a *echo.Group) { u.GET("/sessions", sessionProvider.ReadAllWeb) u.DELETE("/sessions/:session", sessionProvider.DeleteWeb) + // User-level webhooks + if config.WebhooksEnabled.GetBool() { + u.GET("/settings/webhooks", apiv1.GetUserWebhooks) + u.GET("/settings/webhooks/events", apiv1.GetUserDirectedWebhookEvents) + u.PUT("/settings/webhooks", apiv1.CreateUserWebhook) + u.POST("/settings/webhooks/:webhook", apiv1.UpdateUserWebhook) + u.DELETE("/settings/webhooks/:webhook", apiv1.DeleteUserWebhook) + } + if config.ServiceEnableTotp.GetBool() { u.GET("/settings/totp", apiv1.UserTOTP) u.POST("/settings/totp/enroll", apiv1.UserTOTPEnroll)