vikunja-api/pkg/routes/routes.go

450 lines
14 KiB
Go
Raw Permalink Normal View History

2020-02-07 17:27:45 +01:00
// Vikunja is a to-do list application to facilitate your life.
2020-01-09 18:33:22 +01:00
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
2018-11-26 21:17:33 +01:00
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
2018-11-26 21:17:33 +01:00
//
// 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 General Public License for more details.
2018-11-26 21:17:33 +01:00
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2018-11-26 21:17:33 +01:00
// @title Vikunja API
2020-02-07 17:27:45 +01:00
// @description This is the documentation for the [Vikunja](http://vikunja.io) API. Vikunja is a cross-plattform To-do-application with a lot of features, such as sharing lists with users or teams. <!-- ReDoc-Inject: <security-definitions> -->
2019-10-23 23:11:40 +02:00
// @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.
2019-01-03 23:22:06 +01:00
// @description # Authorization
2019-10-16 22:52:29 +02:00
// @description **JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer <jwt-token>`-header to authenticate successfully.
2019-01-03 23:22:06 +01:00
// @description
// @description **BasicAuth:** Only used when requesting tasks via caldav.
// @description <!-- ReDoc-Inject: <security-definitions> -->
// @BasePath /api/v1
2019-01-03 23:22:06 +01:00
// @license.url http://code.vikunja.io/api/src/branch/master/LICENSE
// @license.name GPLv3
// @contact.url http://vikunja.io/en/contact/
// @contact.name General Vikunja contact
// @contact.email hello@vikunja.io
// @securityDefinitions.basic BasicAuth
2019-01-03 23:22:06 +01:00
// @securityDefinitions.apikey JWTKeyAuth
// @in header
// @name Authorization
2018-06-10 11:11:41 +02:00
2018-08-29 15:22:17 +02:00
package routes
2018-06-10 11:11:41 +02:00
import (
"code.vikunja.io/api/pkg/config"
2018-12-01 00:26:56 +01:00
"code.vikunja.io/api/pkg/log"
2018-11-17 00:17:37 +01:00
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
"code.vikunja.io/api/pkg/modules/migration/wunderlist"
2018-10-31 13:42:38 +01:00
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
2019-05-22 19:48:48 +02:00
"code.vikunja.io/api/pkg/routes/caldav"
2019-02-17 20:53:04 +01:00
_ "code.vikunja.io/api/pkg/swagger" // To generate swagger docs
"code.vikunja.io/api/pkg/user"
2018-12-01 00:26:56 +01:00
"code.vikunja.io/web"
"code.vikunja.io/web/handler"
2018-11-17 00:17:37 +01:00
"github.com/asaskevich/govalidator"
2019-05-07 21:42:24 +02:00
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
2019-01-25 12:40:54 +01:00
elog "github.com/labstack/gommon/log"
2019-05-22 19:48:48 +02:00
"strings"
2018-06-10 11:11:41 +02:00
)
2018-11-17 00:17:37 +01:00
// CustomValidator is a dummy struct to use govalidator with echo
type CustomValidator struct{}
// Validate validates stuff
func (cv *CustomValidator) Validate(i interface{}) error {
if _, err := govalidator.ValidateStruct(i); err != nil {
var errs []string
for field, e := range govalidator.ErrorsByField(err) {
errs = append(errs, field+": "+e)
}
httperr := models.ValidationHTTPError{
HTTPError: web.HTTPError{
2018-11-17 00:17:37 +01:00
Code: models.ErrCodeInvalidData,
Message: "Invalid Data",
},
InvalidFields: errs,
2018-11-17 00:17:37 +01:00
}
return httperr
}
return nil
}
2018-06-10 11:11:41 +02:00
// NewEcho registers a new Echo instance
func NewEcho() *echo.Echo {
e := echo.New()
2018-11-03 16:05:45 +01:00
e.HideBanner = true
2019-01-25 12:40:54 +01:00
if l, ok := e.Logger.(*elog.Logger); ok {
if config.LogEcho.GetString() == "off" {
2019-01-25 12:40:54 +01:00
l.SetLevel(elog.OFF)
}
l.EnableColor()
l.SetHeader(log.ErrFmt)
l.SetOutput(log.GetLogWriter("echo"))
}
2018-06-10 11:11:41 +02:00
// Logger
if config.LogHTTP.GetString() != "off" {
2019-01-25 12:40:54 +01:00
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: log.WebFmt + "\n",
Output: log.GetLogWriter("http"),
}))
}
2018-06-10 11:11:41 +02:00
2018-11-17 00:17:37 +01:00
// Validation
e.Validator = &CustomValidator{}
2019-03-24 10:13:40 +01:00
// Handler config
handler.SetAuthProvider(&web.Auths{
AuthObject: apiv1.GetAuthFromClaims,
2019-03-24 10:13:40 +01:00
})
2019-07-20 20:12:10 +02:00
handler.SetLoggingProvider(log.GetLogger())
2019-10-23 23:11:40 +02:00
handler.SetMaxItemsPerPage(config.ServiceMaxItemsPerPage.GetInt())
2019-03-24 10:13:40 +01:00
2018-06-10 11:11:41 +02:00
return e
}
// RegisterRoutes registers all routes for the application
func RegisterRoutes(e *echo.Echo) {
if config.ServiceEnableCaldav.GetBool() {
2019-05-22 19:48:48 +02:00
// Caldav routes
wkg := e.Group("/.well-known")
wkg.Use(middleware.BasicAuth(caldavBasicAuth))
wkg.Any("/caldav", caldav.PrincipalHandler)
wkg.Any("/caldav/", caldav.PrincipalHandler)
c := e.Group("/dav")
registerCalDavRoutes(c)
}
2018-09-07 22:49:16 +02:00
// CORS_SHIT
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")
},
}))
}
2018-06-10 11:11:41 +02:00
// API Routes
a := e.Group("/api/v1")
2019-05-22 19:48:48 +02:00
registerAPIRoutes(a)
}
func registerAPIRoutes(a *echo.Group) {
2018-06-10 11:11:41 +02:00
// 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")
2019-01-03 23:22:06 +01:00
// Docs
n.GET("/docs.json", apiv1.DocsJSON)
n.GET("/docs", apiv1.RedocUI)
2018-09-17 18:34:46 +02:00
// Prometheus endpoint
setupMetrics(n)
// User stuff
n.POST("/login", apiv1.Login)
n.POST("/register", apiv1.RegisterUser)
n.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
n.POST("/user/password/reset", apiv1.UserResetPassword)
n.POST("/user/confirm", apiv1.UserConfirmEmail)
2018-06-10 11:11:41 +02:00
2019-07-16 00:54:38 +02:00
// Info endpoint
n.GET("/info", apiv1.Info)
2019-07-16 00:54:38 +02:00
// Avatar endpoint
n.GET("/:username/avatar", apiv1.GetAvatar)
// Link share auth
if config.ServiceEnableLinkSharing.GetBool() {
n.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare)
}
2019-07-21 23:27:30 +02:00
// ===== Routes with Authetication =====
2018-06-10 11:11:41 +02:00
// Authetification
a.Use(middleware.JWT([]byte(config.ServiceJWTSecret.GetString())))
2018-12-01 00:26:56 +01:00
2019-07-21 23:27:30 +02:00
// Rate limit
setupRateLimit(a, config.RateLimitKind.GetString())
2019-07-21 23:27:30 +02:00
// Middleware to collect metrics
2019-07-21 23:44:12 +02:00
setupMetricsMiddleware(a)
2018-06-10 11:11:41 +02:00
a.POST("/tokenTest", apiv1.CheckToken)
2018-06-10 14:14:10 +02:00
2018-09-20 19:42:01 +02:00
// User stuff
a.GET("/user", apiv1.UserShow)
a.POST("/user/password", apiv1.UserChangePassword)
2018-09-20 19:42:01 +02:00
a.GET("/users", apiv1.UserList)
2019-12-07 20:52:04 +01:00
a.POST("/user/token", apiv1.RenewToken)
2018-09-20 19:42:01 +02:00
2018-12-01 00:26:56 +01:00
listHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.List{}
},
2018-07-07 14:19:34 +02:00
}
a.GET("/lists", listHandler.ReadAllWeb)
2018-07-21 15:08:46 +02:00
a.GET("/lists/:list", listHandler.ReadOneWeb)
a.POST("/lists/:list", listHandler.UpdateWeb)
a.DELETE("/lists/:list", listHandler.DeleteWeb)
a.PUT("/namespaces/:namespace/lists", listHandler.CreateWeb)
a.GET("/lists/:list/listusers", apiv1.ListUsersForList)
if config.ServiceEnableLinkSharing.GetBool() {
listSharingHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.LinkSharing{}
},
}
a.PUT("/lists/:list/shares", listSharingHandler.CreateWeb)
a.GET("/lists/:list/shares", listSharingHandler.ReadAllWeb)
a.GET("/lists/:list/shares/:share", listSharingHandler.ReadOneWeb)
a.DELETE("/lists/:list/shares/:share", listSharingHandler.DeleteWeb)
}
2019-12-01 14:38:11 +01:00
taskCollectionHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TaskCollection{}
},
}
a.GET("/lists/:list/tasks", taskCollectionHandler.ReadAllWeb)
2018-12-01 00:26:56 +01:00
taskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
2019-08-14 22:19:04 +02:00
return &models.Task{}
},
}
2018-08-30 08:09:17 +02:00
a.PUT("/lists/:list", taskHandler.CreateWeb)
a.GET("/tasks/:listtask", taskHandler.ReadOneWeb)
2019-12-01 14:38:11 +01:00
a.GET("/tasks/all", taskCollectionHandler.ReadAllWeb)
2018-08-30 08:09:17 +02:00
a.DELETE("/tasks/:listtask", taskHandler.DeleteWeb)
a.POST("/tasks/:listtask", taskHandler.UpdateWeb)
2018-12-28 22:49:46 +01:00
bulkTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.BulkTask{}
},
}
a.POST("/tasks/bulk", bulkTaskHandler.UpdateWeb)
2019-01-08 20:13:07 +01:00
assigneeTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
2019-08-14 22:19:04 +02:00
return &models.TaskAssginee{}
2019-01-08 20:13:07 +01:00
},
}
a.PUT("/tasks/:listtask/assignees", assigneeTaskHandler.CreateWeb)
a.DELETE("/tasks/:listtask/assignees/:user", assigneeTaskHandler.DeleteWeb)
a.GET("/tasks/:listtask/assignees", assigneeTaskHandler.ReadAllWeb)
bulkAssigneeHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.BulkAssignees{}
},
}
2019-01-10 00:08:12 +01:00
a.POST("/tasks/:listtask/assignees/bulk", bulkAssigneeHandler.CreateWeb)
2019-01-08 20:13:07 +01:00
2018-12-31 02:18:41 +01:00
labelTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.LabelTask{}
},
}
a.PUT("/tasks/:listtask/labels", labelTaskHandler.CreateWeb)
a.DELETE("/tasks/:listtask/labels/:label", labelTaskHandler.DeleteWeb)
a.GET("/tasks/:listtask/labels", labelTaskHandler.ReadAllWeb)
2019-01-10 00:08:12 +01:00
bulkLabelTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.LabelTaskBulk{}
},
}
a.POST("/tasks/:listtask/labels/bulk", bulkLabelTaskHandler.CreateWeb)
2019-09-25 20:44:41 +02:00
taskRelationHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TaskRelation{}
},
}
a.PUT("/tasks/:task/relations", taskRelationHandler.CreateWeb)
a.DELETE("/tasks/:task/relations", 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)
2019-10-16 22:52:29 +02:00
}
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)
}
2018-12-31 02:18:41 +01:00
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)
2018-12-01 00:26:56 +01:00
listTeamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TeamList{}
},
}
a.GET("/lists/:list/teams", listTeamHandler.ReadAllWeb)
a.PUT("/lists/:list/teams", listTeamHandler.CreateWeb)
a.DELETE("/lists/:list/teams/:team", listTeamHandler.DeleteWeb)
a.POST("/lists/:list/teams/:team", listTeamHandler.UpdateWeb)
2018-12-01 00:26:56 +01:00
listUserHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.ListUser{}
},
}
a.GET("/lists/:list/users", listUserHandler.ReadAllWeb)
a.PUT("/lists/:list/users", listUserHandler.CreateWeb)
a.DELETE("/lists/:list/users/:user", listUserHandler.DeleteWeb)
a.POST("/lists/:list/users/:user", listUserHandler.UpdateWeb)
2018-12-01 00:26:56 +01:00
namespaceHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Namespace{}
},
}
a.GET("/namespaces", namespaceHandler.ReadAllWeb)
a.PUT("/namespaces", namespaceHandler.CreateWeb)
2018-07-21 15:08:46 +02:00
a.GET("/namespaces/:namespace", namespaceHandler.ReadOneWeb)
a.POST("/namespaces/:namespace", namespaceHandler.UpdateWeb)
a.DELETE("/namespaces/:namespace", namespaceHandler.DeleteWeb)
a.GET("/namespaces/:namespace/lists", apiv1.GetListsByNamespaceID)
2018-07-14 17:34:59 +02:00
2018-12-01 00:26:56 +01:00
namespaceTeamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TeamNamespace{}
},
}
2018-07-18 21:54:04 +02:00
a.GET("/namespaces/:namespace/teams", namespaceTeamHandler.ReadAllWeb)
a.PUT("/namespaces/:namespace/teams", namespaceTeamHandler.CreateWeb)
a.DELETE("/namespaces/:namespace/teams/:team", namespaceTeamHandler.DeleteWeb)
a.POST("/namespaces/:namespace/teams/:team", namespaceTeamHandler.UpdateWeb)
2018-12-01 00:26:56 +01:00
namespaceUserHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.NamespaceUser{}
},
2018-09-04 20:15:24 +02:00
}
a.GET("/namespaces/:namespace/users", namespaceUserHandler.ReadAllWeb)
a.PUT("/namespaces/:namespace/users", namespaceUserHandler.CreateWeb)
a.DELETE("/namespaces/:namespace/users/:user", namespaceUserHandler.DeleteWeb)
a.POST("/namespaces/:namespace/users/:user", namespaceUserHandler.UpdateWeb)
2018-09-04 20:15:24 +02:00
2018-12-01 00:26:56 +01:00
teamHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.Team{}
},
2018-07-14 17:34:59 +02:00
}
a.GET("/teams", teamHandler.ReadAllWeb)
2018-07-21 15:08:46 +02:00
a.GET("/teams/:team", teamHandler.ReadOneWeb)
2018-07-14 17:34:59 +02:00
a.PUT("/teams", teamHandler.CreateWeb)
2018-07-21 15:08:46 +02:00
a.POST("/teams/:team", teamHandler.UpdateWeb)
a.DELETE("/teams/:team", teamHandler.DeleteWeb)
2018-07-26 09:53:32 +02:00
2018-12-01 00:26:56 +01:00
teamMemberHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.TeamMember{}
},
2018-07-26 09:53:32 +02:00
}
a.PUT("/teams/:team/members", teamMemberHandler.CreateWeb)
a.DELETE("/teams/:team/members/:user", teamMemberHandler.DeleteWeb)
// Migrations
m := a.Group("/migration")
// Wunderlist
if config.MigrationWunderlistEnable.GetBool() {
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
MigrationStruct: func() migration.Migrator {
return &wunderlist.Migration{}
},
}
wunderlistMigrationHandler.RegisterRoutes(m)
}
2018-06-10 11:11:41 +02:00
}
2019-05-22 19:48:48 +02:00
func registerCalDavRoutes(c *echo.Group) {
// Basic auth middleware
c.Use(middleware.BasicAuth(caldavBasicAuth))
// THIS is the entry point for caldav clients, otherwise lists will show up double
c.Any("", caldav.EntryHandler)
c.Any("/", caldav.EntryHandler)
c.Any("/principals/*/", caldav.PrincipalHandler)
c.Any("/lists", caldav.ListHandler)
c.Any("/lists/", caldav.ListHandler)
c.Any("/lists/:list", caldav.ListHandler)
c.Any("/lists/:list/", caldav.ListHandler)
c.Any("/lists/:list/:task", caldav.TaskHandler) // Mostly used for editing
}
func caldavBasicAuth(username, password string, c echo.Context) (bool, error) {
creds := &user.Login{
2019-05-22 19:48:48 +02:00
Username: username,
Password: password,
}
u, err := user.CheckUserCredentials(creds)
2019-05-22 19:48:48 +02:00
if err != nil {
2019-07-20 20:12:10 +02:00
log.Errorf("Error during basic auth for caldav: %v", err)
2019-05-22 19:48:48 +02:00
return false, nil
}
// Save the user in echo context for later use
c.Set("userBasicAuth", u)
return true, nil
}