feat(api tokens): add crud routes to manage api tokens

This commit is contained in:
kolaente 2023-08-31 20:37:25 +02:00
parent 3faf48706a
commit e6b25bd57b
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
5 changed files with 154 additions and 9 deletions

View File

@ -17,8 +17,13 @@
package models
import (
"code.vikunja.io/web"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
"xorm.io/xorm"
)
type APIPermissions map[string][]string
@ -28,13 +33,13 @@ type APIToken struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"token"`
// A human-readable name for this token
Title string `xorm:"not null" json:"title"`
Title string `xorm:"not null" json:"title" valid:"required"`
// The actual api key. Only visible after creation.
Key string `xorm:"not null varchar(50)" json:"key,omitempty"`
// The permissions this key has. Possible values are available via the /routes endpoint and consist of the keys of the list from that endpoint. For example, if the token should be able to read all tasks as well as update existing tasks, you should add `{"tasks":["read_all","update"]}`.
Permissions APIPermissions `xorm:"json not null" json:"permissions"`
// The permissions this token has. Possible values are available via the /routes endpoint and consist of the keys of the list from that endpoint. For example, if the token should be able to read all tasks as well as update existing tasks, you should add `{"tasks":["read_all","update"]}`.
Permissions APIPermissions `xorm:"json not null" json:"permissions" valid:"required"`
// The date when this key expires.
ExpiresAt time.Time `xorm:"not null" json:"expires_at"`
ExpiresAt time.Time `xorm:"not null" json:"expires_at" valid:"required"`
// A timestamp when this api key was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
@ -50,3 +55,87 @@ type APIToken struct {
func (*APIToken) TableName() string {
return "api_tokens"
}
func GetAPITokenByID(s *xorm.Session, id int64) (token *APIToken, err error) {
token = &APIToken{}
_, err = s.Where("id = ?", id).
Get(token)
return
}
// Create creates a new token
// @Summary Create a new api token
// @Description Create a new api token to use on behalf of the user creating it.
// @tags api
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param token body models.APIToken true "The token object with required fields"
// @Success 200 {object} models.APIToken "The created token."
// @Failure 400 {object} web.HTTPError "Invalid token object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tokens [put]
func (t *APIToken) Create(s *xorm.Session, a web.Auth) (err error) {
t.ID = 0
t.Key = "tk_" + utils.MakeRandomString(32)
t.OwnerID = a.GetID()
// TODO: validate permissions
_, err = s.Insert(t)
return err
}
// ReadAll returns all api tokens the current user has created
// @Summary Get all api tokens of the current user
// @Description Returns all api tokens the current user has created.
// @tags api
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @Success 200 {array} models.APIToken "The list of all tokens"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /tokens [get]
func (t *APIToken) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
tokens := []*APIToken{}
query := s.Where("owner_id = ?", a.GetID()).
Limit(getLimitFromPageIndex(page, perPage))
if search != "" {
query = query.Where(db.ILIKE("title", search))
}
err = query.Find(&tokens)
if err != nil {
return nil, 0, 0, err
}
for _, token := range tokens {
token.Key = ""
}
totalCount, err := query.Count(&APIToken{})
return tokens, len(tokens), totalCount, err
}
// Delete deletes a token
// @Summary Deletes an existing api token
// @Description Delete any of the user's api tokens.
// @tags api
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param tokenID path int true "Token ID"
// @Success 200 {object} models.Message "Successfully deleted."
// @Failure 404 {object} web.HTTPError "The token does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tokens/{tokenID} [delete]
func (t *APIToken) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Where("id = ? AND owner_id = ?", t.ID, a.GetID()).Delete(&APIToken{})
return err
}

View File

@ -0,0 +1,40 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2023 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 (
"code.vikunja.io/web"
"xorm.io/xorm"
)
func (t *APIToken) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
token, err := GetAPITokenByID(s, t.ID)
if err != nil {
return false, err
}
if token.OwnerID != a.GetID() {
return false, nil
}
*t = *token
return true, nil
}
func (t *APIToken) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
return true, nil
}

View File

@ -22,6 +22,8 @@ import (
"net/http"
"code.vikunja.io/api/pkg/log"
_ "code.vikunja.io/api/pkg/swagger" // To make sure the swag files are properly registered
"github.com/labstack/echo/v4"
"github.com/swaggo/swag"
)

View File

@ -17,9 +17,10 @@
package routes
import (
"github.com/labstack/echo/v4"
"net/http"
"strings"
"github.com/labstack/echo/v4"
)
var apiTokenRoutes = map[string]*APITokenRoute{}
@ -56,7 +57,7 @@ func getRouteGroupName(route echo.Route) string {
}
}
// gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
// collectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
func collectRoutesForAPITokenUsage(route echo.Route) {
if !strings.Contains(route.Name, "(*WebHandler)") {
@ -65,7 +66,10 @@ func collectRoutesForAPITokenUsage(route echo.Route) {
routeGroupName := getRouteGroupName(route)
if routeGroupName == "subscriptions" || routeGroupName == "notifications" || strings.HasSuffix(routeGroupName, "_bulk") {
if routeGroupName == "subscriptions" ||
routeGroupName == "notifications" ||
routeGroupName == "tokens" ||
strings.HasSuffix(routeGroupName, "_bulk") {
return
}
@ -94,7 +98,7 @@ func collectRoutesForAPITokenUsage(route echo.Route) {
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.
// @Summary Get a list of all token api routes
// @Description Returns a list of all API routes which are available to use with an api token, not a user login.
// @tags opi
// @tags api
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {array} routes.APITokenRoute "The list of all routes."

View File

@ -563,6 +563,16 @@ func registerAPIRoutes(a *echo.Group) {
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)
}
func registerMigrations(m *echo.Group) {