From 882d4c9d6f316286b3b648e03cd045987c28de1b Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:06:01 +0100 Subject: [PATCH 1/9] feat: add user caldav token generation --- pkg/routes/api/v1/user_caldav_token.go | 83 ++++++++++++++++++++++++++ pkg/routes/routes.go | 2 + pkg/user/caldav_token.go | 42 +++++++++++++ pkg/user/delete.go | 2 +- pkg/user/token.go | 37 +++++++++--- pkg/user/update_email.go | 2 +- pkg/user/user_create.go | 2 +- pkg/user/user_password_reset.go | 2 +- 8 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 pkg/routes/api/v1/user_caldav_token.go create mode 100644 pkg/user/caldav_token.go diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go new file mode 100644 index 000000000..85ab78df4 --- /dev/null +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -0,0 +1,83 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 . + +package v1 + +import ( + "code.vikunja.io/api/pkg/user" + "code.vikunja.io/web/handler" + "github.com/labstack/echo/v4" + "net/http" +) + +type TokenResponse struct { + Token string `json:"token"` +} + +// GenerateCaldavToken is the handler to create a caldav token +// @Summary Generate a caldav token +// @Description Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated. +// @tags user +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Success 200 {object} v1.TokenResponse +// @Failure 400 {object} web.HTTPError "Something's invalid." +// @Failure 404 {object} web.HTTPError "User does not exist." +// @Failure 500 {object} models.Message "Internal server error." +// @Router /user/settings/token/caldav [post] +func GenerateCaldavToken(c echo.Context) (err error) { + + u, err := user.GetCurrentUser(c) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + token, err := user.GenerateNewCaldavToken(u) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + return c.JSON(http.StatusCreated, &TokenResponse{ + Token: token.ClearTextToken, + }) +} + +// GetCaldavTokens is the handler to return a list of all caldav tokens for the current user +// @Summary Returns the caldav tokens for the current user +// @Description Return the IDs and created dates of all caldav tokens for the current user. +// @tags user +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Success 200 {array} user.Token +// @Failure 400 {object} web.HTTPError "Something's invalid." +// @Failure 404 {object} web.HTTPError "User does not exist." +// @Failure 500 {object} models.Message "Internal server error." +// @Router /user/settings/token/caldav [get] +func GetCaldavTokens(c echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + tokens, err := user.GetCaldavTokens(u) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + return c.JSON(http.StatusCreated, tokens) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 46b6c88d1..f211d3be3 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -323,6 +323,8 @@ func registerAPIRoutes(a *echo.Group) { u.POST("/export/request", apiv1.RequestUserDataExport) u.POST("/export/download", apiv1.DownloadUserDataExport) u.GET("/timezones", apiv1.GetAvailableTimezones) + u.POST("/settings/token/caldav", apiv1.GenerateCaldavToken) + u.GET("/settings/token/caldav", apiv1.GetCaldavTokens) if config.ServiceEnableTotp.GetBool() { u.GET("/settings/totp", apiv1.UserTOTP) diff --git a/pkg/user/caldav_token.go b/pkg/user/caldav_token.go new file mode 100644 index 000000000..1beb660a3 --- /dev/null +++ b/pkg/user/caldav_token.go @@ -0,0 +1,42 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 . + +package user + +import "code.vikunja.io/api/pkg/db" + +func GenerateNewCaldavToken(u *User) (token *Token, err error) { + s := db.NewSession() + + t, err := generateHashedToken(s, u, TokenCaldavAuth) + if err != nil { + return nil, err + } + + return t, err +} + +func GetCaldavTokens(u *User) (tokens []*Token, err error) { + + s := db.NewSession() + + t, err := getTokensForKind(s, u, TokenCaldavAuth) + if err != nil { + return nil, err + } + + return t, err +} diff --git a/pkg/user/delete.go b/pkg/user/delete.go index c54160ded..3e1727208 100644 --- a/pkg/user/delete.go +++ b/pkg/user/delete.go @@ -87,7 +87,7 @@ func notifyUsersScheduledForDeletion() { // RequestDeletion creates a user deletion confirm token and sends a notification to the user func RequestDeletion(s *xorm.Session, user *User) (err error) { - token, err := generateNewToken(s, user, TokenAccountDeletion) + token, err := generateToken(s, user, TokenAccountDeletion) if err != nil { return err } diff --git a/pkg/user/token.go b/pkg/user/token.go index 076df9532..c7d9a277c 100644 --- a/pkg/user/token.go +++ b/pkg/user/token.go @@ -34,17 +34,19 @@ const ( TokenPasswordReset TokenEmailConfirm TokenAccountDeletion + TokenCaldavAuth tokenSize = 64 ) // Token is a token a user can use to do things like verify their email or resetting their password type Token struct { - ID int64 `xorm:"bigint autoincr not null unique pk"` - UserID int64 `xorm:"not null"` - Token string `xorm:"varchar(450) not null index"` - Kind TokenKind `xorm:"not null"` - Created time.Time `xorm:"created not null"` + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` + UserID int64 `xorm:"not null" json:"-"` + Token string `xorm:"varchar(450) not null index" json:"-"` + ClearTextToken string `xorm:"-" json:"-"` + Kind TokenKind `xorm:"not null" json:"-"` + Created time.Time `xorm:"created not null" json:"created"` } // TableName returns the real table name for user tokens @@ -52,12 +54,25 @@ func (t *Token) TableName() string { return "user_tokens" } -func generateNewToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) { - token = &Token{ +func genToken(u *User, kind TokenKind) *Token { + return &Token{ UserID: u.ID, Kind: kind, Token: utils.MakeRandomString(tokenSize), } +} + +func generateToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) { + token = genToken(u, kind) + + _, err = s.Insert(token) + return +} + +func generateHashedToken(s *xorm.Session, u *User, kind TokenKind) (token *Token, err error) { + token = genToken(u, kind) + token.ClearTextToken = token.Token + token.Token, err = HashPassword(token.ClearTextToken) _, err = s.Insert(token) return @@ -74,6 +89,14 @@ func getToken(s *xorm.Session, token string, kind TokenKind) (t *Token, err erro return } +func getTokensForKind(s *xorm.Session, u *User, kind TokenKind) (tokens []*Token, err error) { + tokens = []*Token{} + + err = s.Where("kind = ? AND user_id = ?", kind, u.ID). + Find(&tokens) + return +} + func removeTokens(s *xorm.Session, u *User, kind TokenKind) (err error) { _, err = s.Where("user_id = ? AND kind = ?", u.ID, kind). Delete(&Token{}) diff --git a/pkg/user/update_email.go b/pkg/user/update_email.go index 6b4b35d9b..f1572ec1b 100644 --- a/pkg/user/update_email.go +++ b/pkg/user/update_email.go @@ -63,7 +63,7 @@ func UpdateEmail(s *xorm.Session, update *EmailUpdate) (err error) { } update.User.Status = StatusEmailConfirmationRequired - token, err := generateNewToken(s, update.User, TokenEmailConfirm) + token, err := generateToken(s, update.User, TokenEmailConfirm) if err != nil { return } diff --git a/pkg/user/user_create.go b/pkg/user/user_create.go index 1ccecdafd..213e9cee1 100644 --- a/pkg/user/user_create.go +++ b/pkg/user/user_create.go @@ -81,7 +81,7 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) { } user.Status = StatusEmailConfirmationRequired - token, err := generateNewToken(s, user, TokenEmailConfirm) + token, err := generateToken(s, user, TokenEmailConfirm) if err != nil { return nil, err } diff --git a/pkg/user/user_password_reset.go b/pkg/user/user_password_reset.go index 277d0bb78..0fc3290b6 100644 --- a/pkg/user/user_password_reset.go +++ b/pkg/user/user_password_reset.go @@ -112,7 +112,7 @@ func RequestUserPasswordResetTokenByEmail(s *xorm.Session, tr *PasswordTokenRequ // RequestUserPasswordResetToken sends a user a password reset email. func RequestUserPasswordResetToken(s *xorm.Session, user *User) (err error) { - token, err := generateNewToken(s, user, TokenPasswordReset) + token, err := generateToken(s, user, TokenPasswordReset) if err != nil { return } -- 2.40.1 From 38521dedf18bb5f7298a3815f8f2c4e1e2b468c0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:06:46 +0100 Subject: [PATCH 2/9] fix: lint --- pkg/routes/api/v1/user_caldav_token.go | 3 ++- pkg/user/token.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go index 85ab78df4..fd7b29568 100644 --- a/pkg/routes/api/v1/user_caldav_token.go +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -17,10 +17,11 @@ package v1 import ( + "net/http" + "code.vikunja.io/api/pkg/user" "code.vikunja.io/web/handler" "github.com/labstack/echo/v4" - "net/http" ) type TokenResponse struct { diff --git a/pkg/user/token.go b/pkg/user/token.go index c7d9a277c..341dc3cbb 100644 --- a/pkg/user/token.go +++ b/pkg/user/token.go @@ -73,6 +73,9 @@ func generateHashedToken(s *xorm.Session, u *User, kind TokenKind) (token *Token token = genToken(u, kind) token.ClearTextToken = token.Token token.Token, err = HashPassword(token.ClearTextToken) + if err != nil { + return nil, err + } _, err = s.Insert(token) return -- 2.40.1 From a825dc21e99c2cea0405a06356be94ecf4d8e267 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:09:19 +0100 Subject: [PATCH 3/9] chore: generate swagger docs --- pkg/swagger/docs.go | 112 +++++++++++++++++++++++++++++++++++++++ pkg/swagger/swagger.json | 112 +++++++++++++++++++++++++++++++++++++++ pkg/swagger/swagger.yaml | 73 +++++++++++++++++++++++++ 3 files changed, 297 insertions(+) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index ca64c34a2..1e342ff4f 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7026,6 +7026,99 @@ var doc = `{ } } }, + "/user/settings/token/caldav": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Return the IDs and created dates of all caldav tokens for the current user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Returns the caldav tokens for the current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/user.Token" + } + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "post": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Generate a caldav token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.TokenResponse" + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/settings/totp": { "get": { "security": [ @@ -8911,6 +9004,17 @@ var doc = `{ } } }, + "user.Token": { + "type": "object", + "properties": { + "created": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, "user.User": { "type": "object", "properties": { @@ -8952,6 +9056,14 @@ var doc = `{ } } }, + "v1.TokenResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "v1.UserAvatarProvider": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 2e5d604a3..6187372b2 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7010,6 +7010,99 @@ } } }, + "/user/settings/token/caldav": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Return the IDs and created dates of all caldav tokens for the current user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Returns the caldav tokens for the current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/user.Token" + } + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "post": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Generate a caldav token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.TokenResponse" + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/settings/totp": { "get": { "security": [ @@ -8895,6 +8988,17 @@ } } }, + "user.Token": { + "type": "object", + "properties": { + "created": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, "user.User": { "type": "object", "properties": { @@ -8936,6 +9040,14 @@ } } }, + "v1.TokenResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "v1.UserAvatarProvider": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 5f72c4ce6..781e1ae45 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1203,6 +1203,13 @@ definitions: passcode: type: string type: object + user.Token: + properties: + created: + type: string + id: + type: integer + type: object user.User: properties: created: @@ -1235,6 +1242,11 @@ definitions: password: type: string type: object + v1.TokenResponse: + properties: + token: + type: string + type: object v1.UserAvatarProvider: properties: avatar_provider: @@ -6069,6 +6081,67 @@ paths: summary: Change general user settings of the current user. tags: - user + /user/settings/token/caldav: + get: + consumes: + - application/json + description: Return the IDs and created dates of all caldav tokens for the current + user. + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/user.Token' + type: array + "400": + description: Something's invalid. + schema: + $ref: '#/definitions/web.HTTPError' + "404": + description: User does not exist. + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal server error. + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Returns the caldav tokens for the current user + tags: + - user + post: + consumes: + - application/json + description: Generates a caldav token which can be used for the caldav api. + It is not possible to see the token again after it was generated. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.TokenResponse' + "400": + description: Something's invalid. + schema: + $ref: '#/definitions/web.HTTPError' + "404": + description: User does not exist. + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal server error. + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Generate a caldav token + tags: + - user /user/settings/totp: get: consumes: -- 2.40.1 From 245e5d80bac097ee4533a14ada29cfe3df854f01 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:19:48 +0100 Subject: [PATCH 4/9] feat: use caldav tokens for caldav basic auth --- pkg/routes/caldav/auth.go | 68 +++++++++++++++++++++++++++++++++++++++ pkg/routes/routes.go | 28 ++-------------- pkg/user/caldav_token.go | 3 +- pkg/user/user.go | 2 +- 4 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 pkg/routes/caldav/auth.go diff --git a/pkg/routes/caldav/auth.go b/pkg/routes/caldav/auth.go new file mode 100644 index 000000000..fda9ed644 --- /dev/null +++ b/pkg/routes/caldav/auth.go @@ -0,0 +1,68 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 . + +package caldav + +import ( + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/user" + "errors" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" +) + +func BasicAuth(username, password string, c echo.Context) (bool, error) { + creds := &user.Login{ + Username: username, + Password: password, + } + s := db.NewSession() + defer s.Close() + u, err := user.CheckUserCredentials(s, creds) + if err != nil && !user.IsErrWrongUsernameOrPassword(err) { + log.Errorf("Error during basic auth for caldav: %v", err) + return false, nil + } + + if err == nil { + c.Set("userBasicAuth", u) + return true, nil + } + + tokens, err := user.GetCaldavTokens(u) + if err != nil { + log.Errorf("Error while getting tokens for caldav auth: %v", err) + return false, nil + } + + // Looping over all tokens until we find one that matches + for _, token := range tokens { + err = bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(password)) + if err != nil { + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + continue + } + log.Errorf("Error while verifying tokens for caldav auth: %v", err) + return false, nil + } + + c.Set("userBasicAuth", u) + return true, nil + } + + return false, nil +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index f211d3be3..d79497164 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -75,7 +75,6 @@ import ( apiv1 "code.vikunja.io/api/pkg/routes/api/v1" "code.vikunja.io/api/pkg/routes/caldav" _ "code.vikunja.io/api/pkg/swagger" // To generate swagger docs - "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/version" "code.vikunja.io/web" "code.vikunja.io/web/handler" @@ -194,7 +193,7 @@ func RegisterRoutes(e *echo.Echo) { if config.ServiceEnableCaldav.GetBool() { // Caldav routes wkg := e.Group("/.well-known") - wkg.Use(middleware.BasicAuth(caldavBasicAuth)) + wkg.Use(middleware.BasicAuth(caldav.BasicAuth)) wkg.Any("/caldav", caldav.PrincipalHandler) wkg.Any("/caldav/", caldav.PrincipalHandler) c := e.Group("/dav") @@ -665,7 +664,7 @@ func registerMigrations(m *echo.Group) { func registerCalDavRoutes(c *echo.Group) { // Basic auth middleware - c.Use(middleware.BasicAuth(caldavBasicAuth)) + c.Use(middleware.BasicAuth(caldav.BasicAuth)) // THIS is the entry point for caldav clients, otherwise lists will show up double c.Any("", caldav.EntryHandler) @@ -677,26 +676,3 @@ func registerCalDavRoutes(c *echo.Group) { 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{ - Username: username, - Password: password, - } - s := db.NewSession() - defer s.Close() - u, err := user.CheckUserCredentials(s, creds) - if err != nil { - _ = s.Rollback() - log.Errorf("Error during basic auth for caldav: %v", err) - return false, nil - } - - if err := s.Commit(); err != nil { - return false, err - } - - // Save the user in echo context for later use - c.Set("userBasicAuth", u) - return true, nil -} diff --git a/pkg/user/caldav_token.go b/pkg/user/caldav_token.go index 1beb660a3..174cb2d16 100644 --- a/pkg/user/caldav_token.go +++ b/pkg/user/caldav_token.go @@ -20,6 +20,7 @@ import "code.vikunja.io/api/pkg/db" func GenerateNewCaldavToken(u *User) (token *Token, err error) { s := db.NewSession() + defer s.Close() t, err := generateHashedToken(s, u, TokenCaldavAuth) if err != nil { @@ -30,8 +31,8 @@ func GenerateNewCaldavToken(u *User) (token *Token, err error) { } func GetCaldavTokens(u *User) (tokens []*Token, err error) { - s := db.NewSession() + defer s.Close() t, err := getTokensForKind(s, u, TokenCaldavAuth) if err != nil { diff --git a/pkg/user/user.go b/pkg/user/user.go index 1a7c31bfb..7be296e9a 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -324,7 +324,7 @@ func CheckUserCredentials(s *xorm.Session, u *Login) (*User, error) { if IsErrWrongUsernameOrPassword(err) { handleFailedPassword(user) } - return nil, err + return user, err } return user, nil -- 2.40.1 From f0a1b8c71d1c8eb6246c6f8387d2aff7e634b203 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:20:47 +0100 Subject: [PATCH 5/9] fix: lint --- pkg/routes/caldav/auth.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/routes/caldav/auth.go b/pkg/routes/caldav/auth.go index fda9ed644..8b9a9d4ed 100644 --- a/pkg/routes/caldav/auth.go +++ b/pkg/routes/caldav/auth.go @@ -17,10 +17,12 @@ package caldav import ( + "errors" + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" - "errors" + "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) -- 2.40.1 From 5a79e52324f2b00bbcc2bf100fef8b5f73622332 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 17:33:47 +0100 Subject: [PATCH 6/9] feat: add method to remove a token --- pkg/routes/api/v1/user_caldav_token.go | 34 ++++++++++++++++ pkg/routes/routes.go | 1 + pkg/swagger/docs.go | 54 ++++++++++++++++++++++++++ pkg/swagger/swagger.json | 54 ++++++++++++++++++++++++++ pkg/swagger/swagger.yaml | 34 ++++++++++++++++ pkg/user/caldav_token.go | 21 +++++----- pkg/user/token.go | 6 +++ 7 files changed, 192 insertions(+), 12 deletions(-) diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go index fd7b29568..55aeca4f3 100644 --- a/pkg/routes/api/v1/user_caldav_token.go +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -18,6 +18,9 @@ package v1 import ( "net/http" + "strconv" + + "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" "code.vikunja.io/web/handler" @@ -82,3 +85,34 @@ func GetCaldavTokens(c echo.Context) error { return c.JSON(http.StatusCreated, tokens) } + +// DeleteCaldavToken is the handler to delete a caldv token +// @Summary Delete a caldav token by id +// @tags user +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Token ID" +// @Success 200 {object} models.Message +// @Failure 400 {object} web.HTTPError "Something's invalid." +// @Failure 404 {object} web.HTTPError "User does not exist." +// @Failure 500 {object} models.Message "Internal server error." +// @Router /user/settings/token/caldav/{id} [get] +func DeleteCaldavToken(c echo.Context) error { + u, err := user.GetCurrentUser(c) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + err = user.DeleteCaldavTokenByID(u, id) + if err != nil { + return handler.HandleHTTPError(err, c) + } + + return c.JSON(http.StatusOK, &models.Message{Message: "The token was deleted successfully."}) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index d79497164..c5089e01a 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -324,6 +324,7 @@ func registerAPIRoutes(a *echo.Group) { u.GET("/timezones", apiv1.GetAvailableTimezones) u.POST("/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) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 1e342ff4f..a95c87de9 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7119,6 +7119,60 @@ var doc = `{ } } }, + "/user/settings/token/caldav/{id}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete a caldav token by id", + "parameters": [ + { + "type": "integer", + "description": "Token ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/settings/totp": { "get": { "security": [ diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 6187372b2..389e8e110 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7103,6 +7103,60 @@ } } }, + "/user/settings/token/caldav/{id}": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete a caldav token by id", + "parameters": [ + { + "type": "integer", + "description": "Token ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "400": { + "description": "Something's invalid.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "404": { + "description": "User does not exist.", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/settings/totp": { "get": { "security": [ diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 781e1ae45..be677995c 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -6142,6 +6142,40 @@ paths: summary: Generate a caldav token tags: - user + /user/settings/token/caldav/{id}: + get: + consumes: + - application/json + parameters: + - description: Token ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Message' + "400": + description: Something's invalid. + schema: + $ref: '#/definitions/web.HTTPError' + "404": + description: User does not exist. + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal server error. + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Delete a caldav token by id + tags: + - user /user/settings/totp: get: consumes: diff --git a/pkg/user/caldav_token.go b/pkg/user/caldav_token.go index 174cb2d16..4943773ec 100644 --- a/pkg/user/caldav_token.go +++ b/pkg/user/caldav_token.go @@ -22,22 +22,19 @@ func GenerateNewCaldavToken(u *User) (token *Token, err error) { s := db.NewSession() defer s.Close() - t, err := generateHashedToken(s, u, TokenCaldavAuth) - if err != nil { - return nil, err - } - - return t, err + return generateHashedToken(s, u, TokenCaldavAuth) } func GetCaldavTokens(u *User) (tokens []*Token, err error) { s := db.NewSession() defer s.Close() - t, err := getTokensForKind(s, u, TokenCaldavAuth) - if err != nil { - return nil, err - } - - return t, err + return getTokensForKind(s, u, TokenCaldavAuth) +} + +func DeleteCaldavTokenByID(u *User, id int64) error { + s := db.NewSession() + defer s.Close() + + return removeTokenByID(s, u, TokenCaldavAuth, id) } diff --git a/pkg/user/token.go b/pkg/user/token.go index 341dc3cbb..b50f6fa58 100644 --- a/pkg/user/token.go +++ b/pkg/user/token.go @@ -106,6 +106,12 @@ func removeTokens(s *xorm.Session, u *User, kind TokenKind) (err error) { return } +func removeTokenByID(s *xorm.Session, u *User, kind TokenKind, id int64) (err error) { + _, err = s.Where("id = ? AND user_id = ? AND kind = ?", id, u.ID, kind). + Delete(&Token{}) + return +} + // RegisterTokenCleanupCron registers a cron function to clean up all password reset tokens older than 24 hours func RegisterTokenCleanupCron() { const logPrefix = "[User Token Cleanup Cron] " -- 2.40.1 From be19bc5b9021506a7c3d2554ef29e34313a22f6b Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 18:28:47 +0100 Subject: [PATCH 7/9] fix: create token method --- pkg/routes/api/v1/user_caldav_token.go | 2 +- pkg/routes/routes.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go index 55aeca4f3..dde4ab8b3 100644 --- a/pkg/routes/api/v1/user_caldav_token.go +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -42,7 +42,7 @@ type TokenResponse struct { // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." -// @Router /user/settings/token/caldav [post] +// @Router /user/settings/token/caldav [put] func GenerateCaldavToken(c echo.Context) (err error) { u, err := user.GetCurrentUser(c) diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index c5089e01a..d5f79a8c5 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -322,7 +322,7 @@ func registerAPIRoutes(a *echo.Group) { u.POST("/export/request", apiv1.RequestUserDataExport) u.POST("/export/download", apiv1.DownloadUserDataExport) u.GET("/timezones", apiv1.GetAvailableTimezones) - u.POST("/settings/token/caldav", apiv1.GenerateCaldavToken) + u.PUT("/settings/token/caldav", apiv1.GenerateCaldavToken) u.GET("/settings/token/caldav", apiv1.GetCaldavTokens) u.DELETE("/settings/token/caldav/:id", apiv1.DeleteCaldavToken) -- 2.40.1 From 036428c788b00d8a42be0a329ffb076920a7b17a Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 18:30:59 +0100 Subject: [PATCH 8/9] feat: return full token when creating one --- pkg/routes/api/v1/user_caldav_token.go | 10 ++-------- pkg/user/token.go | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/routes/api/v1/user_caldav_token.go b/pkg/routes/api/v1/user_caldav_token.go index dde4ab8b3..adf76a478 100644 --- a/pkg/routes/api/v1/user_caldav_token.go +++ b/pkg/routes/api/v1/user_caldav_token.go @@ -27,10 +27,6 @@ import ( "github.com/labstack/echo/v4" ) -type TokenResponse struct { - Token string `json:"token"` -} - // GenerateCaldavToken is the handler to create a caldav token // @Summary Generate a caldav token // @Description Generates a caldav token which can be used for the caldav api. It is not possible to see the token again after it was generated. @@ -38,7 +34,7 @@ type TokenResponse struct { // @Accept json // @Produce json // @Security JWTKeyAuth -// @Success 200 {object} v1.TokenResponse +// @Success 200 {object} user.Token // @Failure 400 {object} web.HTTPError "Something's invalid." // @Failure 404 {object} web.HTTPError "User does not exist." // @Failure 500 {object} models.Message "Internal server error." @@ -55,9 +51,7 @@ func GenerateCaldavToken(c echo.Context) (err error) { return handler.HandleHTTPError(err, c) } - return c.JSON(http.StatusCreated, &TokenResponse{ - Token: token.ClearTextToken, - }) + return c.JSON(http.StatusCreated, token) } // GetCaldavTokens is the handler to return a list of all caldav tokens for the current user diff --git a/pkg/user/token.go b/pkg/user/token.go index b50f6fa58..dfdb6dc6e 100644 --- a/pkg/user/token.go +++ b/pkg/user/token.go @@ -44,7 +44,7 @@ type Token struct { ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` UserID int64 `xorm:"not null" json:"-"` Token string `xorm:"varchar(450) not null index" json:"-"` - ClearTextToken string `xorm:"-" json:"-"` + ClearTextToken string `xorm:"-" json:"token"` Kind TokenKind `xorm:"not null" json:"-"` Created time.Time `xorm:"created not null" json:"created"` } -- 2.40.1 From ff780f3526950664e545cce2de57d4c4f454e640 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 12 Dec 2021 18:33:03 +0100 Subject: [PATCH 9/9] chore: generate swagger docs --- pkg/swagger/docs.go | 15 +++++---------- pkg/swagger/swagger.json | 15 +++++---------- pkg/swagger/swagger.yaml | 11 ++++------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index a95c87de9..8574cf133 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7074,7 +7074,7 @@ var doc = `{ } } }, - "post": { + "put": { "security": [ { "JWTKeyAuth": [] @@ -7095,7 +7095,7 @@ var doc = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.TokenResponse" + "$ref": "#/definitions/user.Token" } }, "400": { @@ -9066,6 +9066,9 @@ var doc = `{ }, "id": { "type": "integer" + }, + "token": { + "type": "string" } } }, @@ -9110,14 +9113,6 @@ var doc = `{ } } }, - "v1.TokenResponse": { - "type": "object", - "properties": { - "token": { - "type": "string" - } - } - }, "v1.UserAvatarProvider": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 389e8e110..9bc6e72a2 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7058,7 +7058,7 @@ } } }, - "post": { + "put": { "security": [ { "JWTKeyAuth": [] @@ -7079,7 +7079,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.TokenResponse" + "$ref": "#/definitions/user.Token" } }, "400": { @@ -9050,6 +9050,9 @@ }, "id": { "type": "integer" + }, + "token": { + "type": "string" } } }, @@ -9094,14 +9097,6 @@ } } }, - "v1.TokenResponse": { - "type": "object", - "properties": { - "token": { - "type": "string" - } - } - }, "v1.UserAvatarProvider": { "type": "object", "properties": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index be677995c..c4bf8067e 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1209,6 +1209,8 @@ definitions: type: string id: type: integer + token: + type: string type: object user.User: properties: @@ -1242,11 +1244,6 @@ definitions: password: type: string type: object - v1.TokenResponse: - properties: - token: - type: string - type: object v1.UserAvatarProvider: properties: avatar_provider: @@ -6113,7 +6110,7 @@ paths: summary: Returns the caldav tokens for the current user tags: - user - post: + put: consumes: - application/json description: Generates a caldav token which can be used for the caldav api. @@ -6124,7 +6121,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/v1.TokenResponse' + $ref: '#/definitions/user.Token' "400": description: Something's invalid. schema: -- 2.40.1