696 lines
24 KiB
Go
696 lines
24 KiB
Go
// 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/>.
|
|
|
|
// @title Vikunja API
|
|
// @description This is the documentation for the [Vikunja](https://vikunja.io) API. Vikunja is a cross-platform To-do-application with a lot of features, such as sharing projects with users or teams. <!-- ReDoc-Inject: <security-definitions> -->
|
|
|
|
// @description # Pagination
|
|
// @description Every endpoint capable of pagination will return two headers:
|
|
// @description * `x-pagination-total-pages`: The total number of available pages for this request
|
|
// @description * `x-pagination-result-count`: The number of items returned for this request.
|
|
// @description # Rights
|
|
// @description All endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.
|
|
// @description This can be used to show or hide ui elements based on the rights the user has.
|
|
// @description # Errors
|
|
// @description All errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.
|
|
// @description Due to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.
|
|
// @description # Authorization
|
|
// @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.
|
|
// @description
|
|
// @description **API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer <token>` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.
|
|
// @description
|
|
// @description **BasicAuth:** Only used when requesting tasks via CalDAV.
|
|
// @description <!-- ReDoc-Inject: <security-definitions> -->
|
|
// @BasePath /api/v1
|
|
|
|
// @license.url https://code.vikunja.io/api/src/branch/main/LICENSE
|
|
// @license.name AGPL-3.0-or-later
|
|
|
|
// @contact.url https://vikunja.io/contact/
|
|
// @contact.name General Vikunja contact
|
|
// @contact.email hello@vikunja.io
|
|
|
|
// @securityDefinitions.basic BasicAuth
|
|
|
|
// @securityDefinitions.apikey JWTKeyAuth
|
|
// @in header
|
|
// @name Authorization
|
|
|
|
package routes
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/log"
|
|
"code.vikunja.io/api/pkg/models"
|
|
"code.vikunja.io/api/pkg/modules/auth"
|
|
"code.vikunja.io/api/pkg/modules/auth/openid"
|
|
"code.vikunja.io/api/pkg/modules/background"
|
|
backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler"
|
|
"code.vikunja.io/api/pkg/modules/background/unsplash"
|
|
"code.vikunja.io/api/pkg/modules/background/upload"
|
|
"code.vikunja.io/api/pkg/modules/migration"
|
|
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
|
|
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
|
|
"code.vikunja.io/api/pkg/modules/migration/ticktick"
|
|
"code.vikunja.io/api/pkg/modules/migration/todoist"
|
|
"code.vikunja.io/api/pkg/modules/migration/trello"
|
|
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
|
|
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
|
"code.vikunja.io/api/pkg/routes/caldav"
|
|
"code.vikunja.io/api/pkg/version"
|
|
"code.vikunja.io/web"
|
|
"code.vikunja.io/web/handler"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
sentryecho "github.com/getsentry/sentry-go/echo"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
elog "github.com/labstack/gommon/log"
|
|
"github.com/ulule/limiter/v3"
|
|
)
|
|
|
|
// NewEcho registers a new Echo instance
|
|
func NewEcho() *echo.Echo {
|
|
e := echo.New()
|
|
|
|
e.HideBanner = true
|
|
|
|
if l, ok := e.Logger.(*elog.Logger); ok {
|
|
if !config.LogEnabled.GetBool() || config.LogEcho.GetString() == "off" {
|
|
l.SetLevel(elog.OFF)
|
|
}
|
|
l.EnableColor()
|
|
l.SetHeader(log.ErrFmt)
|
|
l.SetOutput(log.GetLogWriter(config.LogEcho.GetString(), "echo"))
|
|
}
|
|
|
|
// Logger
|
|
if !config.LogEnabled.GetBool() || config.LogHTTP.GetString() != "off" {
|
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
Format: log.WebFmt + "\n",
|
|
Output: log.GetLogWriter(config.LogHTTP.GetString(), "http"),
|
|
}))
|
|
}
|
|
|
|
// panic recover
|
|
e.Use(middleware.Recover())
|
|
|
|
setupSentry(e)
|
|
|
|
// Validation
|
|
e.Validator = &CustomValidator{}
|
|
|
|
// Handler config
|
|
handler.SetAuthProvider(&web.Auths{
|
|
AuthObject: auth.GetAuthFromClaims,
|
|
})
|
|
handler.SetLoggingProvider(log.GetLogger())
|
|
handler.SetMaxItemsPerPage(config.ServiceMaxItemsPerPage.GetInt())
|
|
handler.SetSessionFactory(db.NewSession)
|
|
|
|
return e
|
|
}
|
|
|
|
func setupSentry(e *echo.Echo) {
|
|
if !config.SentryEnabled.GetBool() {
|
|
return
|
|
}
|
|
|
|
if err := sentry.Init(sentry.ClientOptions{
|
|
Dsn: config.SentryDsn.GetString(),
|
|
AttachStacktrace: true,
|
|
Release: version.Version,
|
|
}); err != nil {
|
|
log.Criticalf("Sentry init failed: %s", err)
|
|
}
|
|
defer sentry.Flush(5 * time.Second)
|
|
|
|
e.Use(sentryecho.New(sentryecho.Options{
|
|
Repanic: true,
|
|
}))
|
|
|
|
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
|
// Only capture errors not already handled by echo
|
|
var herr *echo.HTTPError
|
|
if errors.As(err, &herr) && herr.Code > 499 {
|
|
hub := sentryecho.GetHubFromContext(c)
|
|
if hub != nil {
|
|
hub.WithScope(func(scope *sentry.Scope) {
|
|
scope.SetExtra("url", c.Request().URL)
|
|
if herr.Internal == nil {
|
|
hub.CaptureException(err)
|
|
} else {
|
|
hub.CaptureException(herr.Internal)
|
|
}
|
|
})
|
|
} else {
|
|
if herr.Internal == nil {
|
|
sentry.CaptureException(err)
|
|
} else {
|
|
sentry.CaptureException(herr.Internal)
|
|
}
|
|
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
|
|
}
|
|
log.Debugf("Error '%s' sent to sentry", err.Error())
|
|
}
|
|
e.DefaultHTTPErrorHandler(err, c)
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers all routes for the application
|
|
func RegisterRoutes(e *echo.Echo) {
|
|
|
|
if config.ServiceEnableCaldav.GetBool() {
|
|
// Caldav routes
|
|
wkg := e.Group("/.well-known")
|
|
wkg.Use(middleware.BasicAuth(caldav.BasicAuth))
|
|
wkg.Any("/caldav", caldav.PrincipalHandler)
|
|
wkg.Any("/caldav/", caldav.PrincipalHandler)
|
|
c := e.Group("/dav")
|
|
registerCalDavRoutes(c)
|
|
}
|
|
|
|
// healthcheck
|
|
e.GET("/health", HealthcheckHandler)
|
|
|
|
setupStaticFrontendFilesHandler(e)
|
|
|
|
// CORS
|
|
if config.CorsEnable.GetBool() {
|
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
|
AllowOrigins: config.CorsOrigins.GetStringSlice(),
|
|
MaxAge: config.CorsMaxAge.GetInt(),
|
|
Skipper: func(context echo.Context) bool {
|
|
// Since it is not possible to register this middleware just for the api group,
|
|
// we just disable it when for caldav requests.
|
|
// Caldav requires OPTIONS requests to be answered in a specific manner,
|
|
// not doing this would break the caldav implementation
|
|
return strings.HasPrefix(context.Path(), "/dav")
|
|
},
|
|
}))
|
|
}
|
|
|
|
// API Routes
|
|
a := e.Group("/api/v1")
|
|
e.OnAddRouteHandler = func(_ string, route echo.Route, _ echo.HandlerFunc, _ []echo.MiddlewareFunc) {
|
|
models.CollectRoutesForAPITokenUsage(route)
|
|
}
|
|
registerAPIRoutes(a)
|
|
}
|
|
|
|
func registerAPIRoutes(a *echo.Group) {
|
|
|
|
// This is the group with no auth
|
|
// It is its own group to be able to rate limit this based on different heuristics
|
|
n := a.Group("")
|
|
setupRateLimit(n, "ip")
|
|
|
|
// Echo does not unescape url path params by default. To make sure values bound as :param in urls are passed
|
|
// properly to handlers, we use this middleware to unescape them.
|
|
// See https://kolaente.dev/vikunja/vikunja/issues/1224
|
|
// See https://github.com/labstack/echo/issues/766
|
|
a.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
params := make([]string, 0, len(c.ParamValues()))
|
|
for _, param := range c.ParamValues() {
|
|
p, err := url.PathUnescape(param)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
params = append(params, p)
|
|
}
|
|
c.SetParamValues(params...)
|
|
return next(c)
|
|
}
|
|
})
|
|
|
|
// Docs
|
|
n.GET("/docs.json", apiv1.DocsJSON)
|
|
n.GET("/docs", apiv1.RedocUI)
|
|
|
|
// Prometheus endpoint
|
|
setupMetrics(n)
|
|
|
|
// Separate route for unauthenticated routes to enable rate limits for it
|
|
ur := a.Group("")
|
|
rate := limiter.Rate{
|
|
Period: 60 * time.Second,
|
|
Limit: config.RateLimitNoAuthRoutesLimit.GetInt64(),
|
|
}
|
|
rateLimiter := createRateLimiter(rate)
|
|
ur.Use(RateLimit(rateLimiter, "ip"))
|
|
|
|
if config.AuthLocalEnabled.GetBool() {
|
|
// User stuff
|
|
ur.POST("/login", apiv1.Login)
|
|
ur.POST("/register", apiv1.RegisterUser)
|
|
ur.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
|
|
ur.POST("/user/password/reset", apiv1.UserResetPassword)
|
|
ur.POST("/user/confirm", apiv1.UserConfirmEmail)
|
|
}
|
|
|
|
if config.AuthOpenIDEnabled.GetBool() {
|
|
ur.POST("/auth/openid/:provider/callback", openid.HandleCallback)
|
|
}
|
|
|
|
// Testing
|
|
if config.ServiceTestingtoken.GetString() != "" {
|
|
n.PATCH("/test/:table", apiv1.HandleTesting)
|
|
}
|
|
|
|
// Info endpoint
|
|
n.GET("/info", apiv1.Info)
|
|
|
|
// Avatar endpoint
|
|
n.GET("/avatar/:username", apiv1.GetAvatar)
|
|
|
|
// Link share auth
|
|
if config.ServiceEnableLinkSharing.GetBool() {
|
|
ur.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare)
|
|
}
|
|
|
|
// ===== Routes with Authentication =====
|
|
a.Use(SetupTokenMiddleware())
|
|
|
|
// Rate limit
|
|
setupRateLimit(a, config.RateLimitKind.GetString())
|
|
|
|
// Middleware to collect metrics
|
|
setupMetricsMiddleware(a)
|
|
|
|
a.POST("/tokenTest", apiv1.CheckToken)
|
|
a.GET("/routes", models.GetAvailableAPIRoutesForToken)
|
|
|
|
// User stuff
|
|
u := a.Group("/user")
|
|
|
|
u.GET("", apiv1.UserShow)
|
|
u.POST("/password", apiv1.UserChangePassword)
|
|
u.GET("s", apiv1.UserList)
|
|
u.POST("/token", apiv1.RenewToken)
|
|
u.POST("/settings/email", apiv1.UpdateUserEmail)
|
|
u.GET("/settings/avatar", apiv1.GetUserAvatarProvider)
|
|
u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider)
|
|
u.PUT("/settings/avatar/upload", apiv1.UploadAvatar)
|
|
u.POST("/settings/general", apiv1.UpdateGeneralUserSettings)
|
|
u.POST("/export/request", apiv1.RequestUserDataExport)
|
|
u.POST("/export/download", apiv1.DownloadUserDataExport)
|
|
u.GET("/timezones", apiv1.GetAvailableTimezones)
|
|
u.PUT("/settings/token/caldav", apiv1.GenerateCaldavToken)
|
|
u.GET("/settings/token/caldav", apiv1.GetCaldavTokens)
|
|
u.DELETE("/settings/token/caldav/:id", apiv1.DeleteCaldavToken)
|
|
|
|
if config.ServiceEnableTotp.GetBool() {
|
|
u.GET("/settings/totp", apiv1.UserTOTP)
|
|
u.POST("/settings/totp/enroll", apiv1.UserTOTPEnroll)
|
|
u.POST("/settings/totp/enable", apiv1.UserTOTPEnable)
|
|
u.POST("/settings/totp/disable", apiv1.UserTOTPDisable)
|
|
u.GET("/settings/totp/qrcode", apiv1.UserTOTPQrCode)
|
|
}
|
|
|
|
// User deletion
|
|
if config.ServiceEnableUserDeletion.GetBool() {
|
|
u.POST("/deletion/request", apiv1.UserRequestDeletion)
|
|
u.POST("/deletion/confirm", apiv1.UserConfirmDeletion)
|
|
u.POST("/deletion/cancel", apiv1.UserCancelDeletion)
|
|
}
|
|
|
|
projectHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Project{}
|
|
},
|
|
}
|
|
a.GET("/projects", projectHandler.ReadAllWeb)
|
|
a.GET("/projects/:project", projectHandler.ReadOneWeb)
|
|
a.POST("/projects/:project", projectHandler.UpdateWeb)
|
|
a.DELETE("/projects/:project", projectHandler.DeleteWeb)
|
|
a.PUT("/projects", projectHandler.CreateWeb)
|
|
a.GET("/projects/:project/projectusers", apiv1.ListUsersForProject)
|
|
|
|
if config.ServiceEnableLinkSharing.GetBool() {
|
|
projectSharingHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.LinkSharing{}
|
|
},
|
|
}
|
|
a.PUT("/projects/:project/shares", projectSharingHandler.CreateWeb)
|
|
a.GET("/projects/:project/shares", projectSharingHandler.ReadAllWeb)
|
|
a.GET("/projects/:project/shares/:share", projectSharingHandler.ReadOneWeb)
|
|
a.DELETE("/projects/:project/shares/:share", projectSharingHandler.DeleteWeb)
|
|
}
|
|
|
|
taskCollectionHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskCollection{}
|
|
},
|
|
}
|
|
a.GET("/projects/:project/views/:view/tasks", taskCollectionHandler.ReadAllWeb)
|
|
a.GET("/projects/:project/tasks", taskCollectionHandler.ReadAllWeb)
|
|
|
|
kanbanBucketHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Bucket{}
|
|
},
|
|
}
|
|
a.GET("/projects/:project/views/:view/buckets", kanbanBucketHandler.ReadAllWeb)
|
|
a.PUT("/projects/:project/views/:view/buckets", kanbanBucketHandler.CreateWeb)
|
|
a.POST("/projects/:project/views/:view/buckets/:bucket", kanbanBucketHandler.UpdateWeb)
|
|
a.DELETE("/projects/:project/views/:view/buckets/:bucket", kanbanBucketHandler.DeleteWeb)
|
|
|
|
projectDuplicateHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.ProjectDuplicate{}
|
|
},
|
|
}
|
|
a.PUT("/projects/:projectid/duplicate", projectDuplicateHandler.CreateWeb)
|
|
|
|
taskHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Task{}
|
|
},
|
|
}
|
|
a.PUT("/projects/:project/tasks", taskHandler.CreateWeb)
|
|
a.GET("/tasks/:projecttask", taskHandler.ReadOneWeb)
|
|
a.GET("/tasks/all", taskCollectionHandler.ReadAllWeb)
|
|
a.DELETE("/tasks/:projecttask", taskHandler.DeleteWeb)
|
|
a.POST("/tasks/:projecttask", taskHandler.UpdateWeb)
|
|
|
|
taskPositionHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskPosition{}
|
|
},
|
|
}
|
|
a.POST("/tasks/:task/position", taskPositionHandler.UpdateWeb)
|
|
|
|
bulkTaskHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.BulkTask{}
|
|
},
|
|
}
|
|
a.POST("/tasks/bulk", bulkTaskHandler.UpdateWeb)
|
|
|
|
assigneeTaskHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskAssginee{}
|
|
},
|
|
}
|
|
a.PUT("/tasks/:projecttask/assignees", assigneeTaskHandler.CreateWeb)
|
|
a.DELETE("/tasks/:projecttask/assignees/:user", assigneeTaskHandler.DeleteWeb)
|
|
a.GET("/tasks/:projecttask/assignees", assigneeTaskHandler.ReadAllWeb)
|
|
|
|
bulkAssigneeHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.BulkAssignees{}
|
|
},
|
|
}
|
|
a.POST("/tasks/:projecttask/assignees/bulk", bulkAssigneeHandler.CreateWeb)
|
|
|
|
labelTaskHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.LabelTask{}
|
|
},
|
|
}
|
|
a.PUT("/tasks/:projecttask/labels", labelTaskHandler.CreateWeb)
|
|
a.DELETE("/tasks/:projecttask/labels/:label", labelTaskHandler.DeleteWeb)
|
|
a.GET("/tasks/:projecttask/labels", labelTaskHandler.ReadAllWeb)
|
|
|
|
bulkLabelTaskHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.LabelTaskBulk{}
|
|
},
|
|
}
|
|
a.POST("/tasks/:projecttask/labels/bulk", bulkLabelTaskHandler.CreateWeb)
|
|
|
|
taskRelationHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskRelation{}
|
|
},
|
|
}
|
|
a.PUT("/tasks/:task/relations", taskRelationHandler.CreateWeb)
|
|
a.DELETE("/tasks/:task/relations/:relationKind/:otherTask", taskRelationHandler.DeleteWeb)
|
|
|
|
if config.ServiceEnableTaskAttachments.GetBool() {
|
|
taskAttachmentHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskAttachment{}
|
|
},
|
|
}
|
|
a.GET("/tasks/:task/attachments", taskAttachmentHandler.ReadAllWeb)
|
|
a.DELETE("/tasks/:task/attachments/:attachment", taskAttachmentHandler.DeleteWeb)
|
|
a.PUT("/tasks/:task/attachments", apiv1.UploadTaskAttachment)
|
|
a.GET("/tasks/:task/attachments/:attachment", apiv1.GetTaskAttachment)
|
|
}
|
|
|
|
if config.ServiceEnableTaskComments.GetBool() {
|
|
taskCommentHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TaskComment{}
|
|
},
|
|
}
|
|
a.GET("/tasks/:task/comments", taskCommentHandler.ReadAllWeb)
|
|
a.PUT("/tasks/:task/comments", taskCommentHandler.CreateWeb)
|
|
a.DELETE("/tasks/:task/comments/:commentid", taskCommentHandler.DeleteWeb)
|
|
a.POST("/tasks/:task/comments/:commentid", taskCommentHandler.UpdateWeb)
|
|
a.GET("/tasks/:task/comments/:commentid", taskCommentHandler.ReadOneWeb)
|
|
}
|
|
|
|
labelHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Label{}
|
|
},
|
|
}
|
|
a.GET("/labels", labelHandler.ReadAllWeb)
|
|
a.GET("/labels/:label", labelHandler.ReadOneWeb)
|
|
a.PUT("/labels", labelHandler.CreateWeb)
|
|
a.DELETE("/labels/:label", labelHandler.DeleteWeb)
|
|
a.POST("/labels/:label", labelHandler.UpdateWeb)
|
|
|
|
projectTeamHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TeamProject{}
|
|
},
|
|
}
|
|
a.GET("/projects/:project/teams", projectTeamHandler.ReadAllWeb)
|
|
a.PUT("/projects/:project/teams", projectTeamHandler.CreateWeb)
|
|
a.DELETE("/projects/:project/teams/:team", projectTeamHandler.DeleteWeb)
|
|
a.POST("/projects/:project/teams/:team", projectTeamHandler.UpdateWeb)
|
|
|
|
projectUserHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.ProjectUser{}
|
|
},
|
|
}
|
|
a.GET("/projects/:project/users", projectUserHandler.ReadAllWeb)
|
|
a.PUT("/projects/:project/users", projectUserHandler.CreateWeb)
|
|
a.DELETE("/projects/:project/users/:user", projectUserHandler.DeleteWeb)
|
|
a.POST("/projects/:project/users/:user", projectUserHandler.UpdateWeb)
|
|
|
|
savedFiltersHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.SavedFilter{}
|
|
},
|
|
}
|
|
a.GET("/filters/:filter", savedFiltersHandler.ReadOneWeb)
|
|
a.PUT("/filters", savedFiltersHandler.CreateWeb)
|
|
a.DELETE("/filters/:filter", savedFiltersHandler.DeleteWeb)
|
|
a.POST("/filters/:filter", savedFiltersHandler.UpdateWeb)
|
|
|
|
teamHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Team{}
|
|
},
|
|
}
|
|
a.GET("/teams", teamHandler.ReadAllWeb)
|
|
a.GET("/teams/:team", teamHandler.ReadOneWeb)
|
|
a.PUT("/teams", teamHandler.CreateWeb)
|
|
a.POST("/teams/:team", teamHandler.UpdateWeb)
|
|
a.DELETE("/teams/:team", teamHandler.DeleteWeb)
|
|
|
|
teamMemberHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.TeamMember{}
|
|
},
|
|
}
|
|
a.PUT("/teams/:team/members", teamMemberHandler.CreateWeb)
|
|
a.DELETE("/teams/:team/members/:user", teamMemberHandler.DeleteWeb)
|
|
a.POST("/teams/:team/members/:user/admin", teamMemberHandler.UpdateWeb)
|
|
|
|
// Subscriptions
|
|
subscriptionHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Subscription{}
|
|
},
|
|
}
|
|
a.PUT("/subscriptions/:entity/:entityID", subscriptionHandler.CreateWeb)
|
|
a.DELETE("/subscriptions/:entity/:entityID", subscriptionHandler.DeleteWeb)
|
|
|
|
// Notifications
|
|
notificationHandler := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.DatabaseNotifications{}
|
|
},
|
|
}
|
|
a.GET("/notifications", notificationHandler.ReadAllWeb)
|
|
a.POST("/notifications/:notificationid", notificationHandler.UpdateWeb)
|
|
a.POST("/notifications", apiv1.MarkAllNotificationsAsRead)
|
|
|
|
// Migrations
|
|
m := a.Group("/migration")
|
|
registerMigrations(m)
|
|
|
|
// Project Backgrounds
|
|
if config.BackgroundsEnabled.GetBool() {
|
|
a.GET("/projects/:project/background", backgroundHandler.GetProjectBackground)
|
|
a.DELETE("/projects/:project/background", backgroundHandler.RemoveProjectBackground)
|
|
if config.BackgroundsUploadEnabled.GetBool() {
|
|
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
|
|
Provider: func() background.Provider {
|
|
return &upload.Provider{}
|
|
},
|
|
}
|
|
a.PUT("/projects/:project/backgrounds/upload", uploadBackgroundProvider.UploadBackground)
|
|
}
|
|
if config.BackgroundsUnsplashEnabled.GetBool() {
|
|
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
|
|
Provider: func() background.Provider {
|
|
return &unsplash.Provider{}
|
|
},
|
|
}
|
|
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
|
|
a.POST("/projects/:project/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
|
|
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
|
|
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
|
|
}
|
|
}
|
|
|
|
// API Tokens
|
|
apiTokenProvider := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.APIToken{}
|
|
},
|
|
}
|
|
a.GET("/tokens", apiTokenProvider.ReadAllWeb)
|
|
a.PUT("/tokens", apiTokenProvider.CreateWeb)
|
|
a.DELETE("/tokens/:token", apiTokenProvider.DeleteWeb)
|
|
|
|
// Webhooks
|
|
if config.WebhooksEnabled.GetBool() {
|
|
webhookProvider := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Webhook{}
|
|
},
|
|
}
|
|
a.GET("/projects/:project/webhooks", webhookProvider.ReadAllWeb)
|
|
a.PUT("/projects/:project/webhooks", webhookProvider.CreateWeb)
|
|
a.DELETE("/projects/:project/webhooks/:webhook", webhookProvider.DeleteWeb)
|
|
a.POST("/projects/:project/webhooks/:webhook", webhookProvider.UpdateWeb)
|
|
a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents)
|
|
}
|
|
|
|
// Reactions
|
|
reactionProvider := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.Reaction{}
|
|
},
|
|
}
|
|
a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb)
|
|
a.POST("/:entitykind/:entityid/reactions/delete", reactionProvider.DeleteWeb)
|
|
a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb)
|
|
|
|
// Project views
|
|
projectViewProvider := &handler.WebHandler{
|
|
EmptyStruct: func() handler.CObject {
|
|
return &models.ProjectView{}
|
|
},
|
|
}
|
|
|
|
a.GET("/projects/:project/views", projectViewProvider.ReadAllWeb)
|
|
a.GET("/projects/:project/views/:view", projectViewProvider.ReadOneWeb)
|
|
a.PUT("/projects/:project/views", projectViewProvider.CreateWeb)
|
|
a.DELETE("/projects/:project/views/:view", projectViewProvider.DeleteWeb)
|
|
a.POST("/projects/:project/views/:view", projectViewProvider.UpdateWeb)
|
|
}
|
|
|
|
func registerMigrations(m *echo.Group) {
|
|
// Todoist
|
|
if config.MigrationTodoistEnable.GetBool() {
|
|
todoistMigrationHandler := &migrationHandler.MigrationWeb{
|
|
MigrationStruct: func() migration.Migrator {
|
|
return &todoist.Migration{}
|
|
},
|
|
}
|
|
todoistMigrationHandler.RegisterMigrator(m)
|
|
}
|
|
|
|
// Trello
|
|
if config.MigrationTrelloEnable.GetBool() {
|
|
trelloMigrationHandler := &migrationHandler.MigrationWeb{
|
|
MigrationStruct: func() migration.Migrator {
|
|
return &trello.Migration{}
|
|
},
|
|
}
|
|
trelloMigrationHandler.RegisterMigrator(m)
|
|
}
|
|
|
|
// Microsoft Todo
|
|
if config.MigrationMicrosoftTodoEnable.GetBool() {
|
|
microsoftTodoMigrationHandler := &migrationHandler.MigrationWeb{
|
|
MigrationStruct: func() migration.Migrator {
|
|
return µsofttodo.Migration{}
|
|
},
|
|
}
|
|
microsoftTodoMigrationHandler.RegisterMigrator(m)
|
|
}
|
|
|
|
// Vikunja File Migrator
|
|
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
|
|
MigrationStruct: func() migration.FileMigrator {
|
|
return &vikunja_file.FileMigrator{}
|
|
},
|
|
}
|
|
vikunjaFileMigrationHandler.RegisterRoutes(m)
|
|
|
|
// TickTick File Migrator
|
|
tickTickFileMigrator := migrationHandler.FileMigratorWeb{
|
|
MigrationStruct: func() migration.FileMigrator {
|
|
return &ticktick.Migrator{}
|
|
},
|
|
}
|
|
tickTickFileMigrator.RegisterRoutes(m)
|
|
}
|
|
|
|
func registerCalDavRoutes(c *echo.Group) {
|
|
|
|
// Basic auth middleware
|
|
c.Use(middleware.BasicAuth(caldav.BasicAuth))
|
|
|
|
// THIS is the entry point for caldav clients, otherwise projects will show up double
|
|
c.Any("", caldav.EntryHandler)
|
|
c.Any("/", caldav.EntryHandler)
|
|
c.Any("/principals/*/", caldav.PrincipalHandler)
|
|
c.Any("/projects", caldav.ProjectHandler)
|
|
c.Any("/projects/", caldav.ProjectHandler)
|
|
c.Any("/projects/:project", caldav.ProjectHandler)
|
|
c.Any("/projects/:project/", caldav.ProjectHandler)
|
|
c.Any("/projects/:project/:task", caldav.TaskHandler) // Mostly used for editing
|
|
}
|