diff --git a/Makefile b/Makefile index da620dbf2b..f3f35f61f5 100644 --- a/Makefile +++ b/Makefile @@ -153,4 +153,27 @@ release-os-package: .PHONY: release-zip release-zip: - $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),cd $(file); zip -r ../../zip/$(shell basename $(file)).zip *; cd ../../../; ) \ No newline at end of file + $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),cd $(file); zip -r ../../zip/$(shell basename $(file)).zip *; cd ../../../; ) + +.PHONY: generate-swagger +generate-swagger: + @hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + $(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \ + fi + swagger generate spec -o ./public/swagger.v1.json + +.PHONY: swagger-check +swagger-check: generate-swagger + @diff=$$(git diff public/swagger.v1.json); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make generate-swagger' and commit the result:"; \ + echo "$${diff}"; \ + exit 1; \ + fi; + +.PHONY: swagger-validate +swagger-validate: + @hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + $(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \ + fi + swagger validate ./public/swagger.v1.json diff --git a/models/error.go b/models/error.go index b14655bbe0..cd514723ac 100644 --- a/models/error.go +++ b/models/error.go @@ -159,7 +159,6 @@ func (err ErrNeedToBeListOwner) Error() string { return fmt.Sprintf("You need to be list owner to do that [ListID: %d, UserID: %d]", err.ListID, err.UserID) } - // ================ // List item errors // ================ @@ -178,7 +177,7 @@ func (err ErrListItemCannotBeEmpty) Error() string { } // ErrListItemCannotBeEmpty represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist. -type ErrListItemDoesNotExist struct{ +type ErrListItemDoesNotExist struct { ID int64 } @@ -206,4 +205,4 @@ func IsErrNeedToBeItemOwner(err error) bool { func (err ErrNeedToBeItemOwner) Error() string { return fmt.Sprintf("You need to be item owner to do that [ItemID: %d, UserID: %d]", err.ItemID, err.UserID) -} \ No newline at end of file +} diff --git a/models/list_delete.go b/models/list_delete.go index 2664a738a2..f1034b4226 100644 --- a/models/list_delete.go +++ b/models/list_delete.go @@ -9,7 +9,7 @@ func DeleteListByID(listID int64, doer *User) (err error) { } if list.Owner.ID != doer.ID { - return ErrNeedToBeListOwner{ListID:listID, UserID:doer.ID} + return ErrNeedToBeListOwner{ListID: listID, UserID: doer.ID} } // Delete the list diff --git a/models/list_items.go b/models/list_items.go index 658414cbd9..d1d4b6c114 100644 --- a/models/list_items.go +++ b/models/list_items.go @@ -95,7 +95,7 @@ func DeleteListItemByID(itemID int64, doer *User) (err error) { // Check if the user hat the right to delete that item if listitem.CreatedByID != doer.ID { - return ErrNeedToBeItemOwner{ItemID:itemID, UserID: doer.ID} + return ErrNeedToBeItemOwner{ItemID: itemID, UserID: doer.ID} } _, err = x.ID(itemID).Delete(ListItem{}) diff --git a/models/list_items_create_update.go b/models/list_items_create_update.go index aa0a1cdc2c..b5e8cefc13 100644 --- a/models/list_items_create_update.go +++ b/models/list_items_create_update.go @@ -17,21 +17,23 @@ func CreateOrUpdateListItem(item *ListItem) (newItem *ListItem, err error) { item.CreatedByID = item.CreatedBy.ID item.CreatedBy = user + // TODO: Check if the user has the right to add/update an item to that list + if item.ID != 0 { _, err = x.ID(item.ID).Update(item) if err != nil { return } } else { - _, err = x.Insert(item) - if err != nil { - return - } - // Check if we have at least a text if item.Text == "" { return newItem, ErrListItemCannotBeEmpty{} } + + _, err = x.Insert(item) + if err != nil { + return + } } // Get the new/updated item diff --git a/models/user.go b/models/user.go index fe49d95a05..0af090828b 100644 --- a/models/user.go +++ b/models/user.go @@ -27,6 +27,23 @@ func (User) TableName() string { return "users" } +// ApiUserPassword represents a user object without timestamps and a json password field. +type ApiUserPassword struct { + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` +} + +func (apiUser *ApiUserPassword) APIFormat() User { + return User{ + ID: apiUser.ID, + Username: apiUser.Username, + Password: apiUser.Password, + Email: apiUser.Email, + } +} + // GetUserByID gets informations about a user by its ID func GetUserByID(id int64) (user User, exists bool, err error) { // Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0 diff --git a/routes/api/v1/item_delete.go b/routes/api/v1/item_delete.go index 384c3892d0..c6e50bb7aa 100644 --- a/routes/api/v1/item_delete.go +++ b/routes/api/v1/item_delete.go @@ -1,13 +1,38 @@ package v1 import ( - "github.com/labstack/echo" - "strconv" - "net/http" "git.kolaente.de/konrad/list/models" + "github.com/labstack/echo" + "net/http" + "strconv" ) func DeleteListItemByIDtemByID(c echo.Context) error { + // swagger:operation DELETE /item/{itemID} lists deleteListItem + // --- + // summary: Deletes a list item + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: itemID + // in: path + // description: ID of the list item to delete + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Message" + // "400": + // "$ref": "#/responses/Message" + // "403": + // "$ref": "#/responses/Message" + // "404": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + // Check if we have our ID id := c.Param("id") // Make int @@ -36,4 +61,4 @@ func DeleteListItemByIDtemByID(c echo.Context) error { } return c.JSON(http.StatusOK, models.Message{"The item was deleted with success."}) -} \ No newline at end of file +} diff --git a/routes/api/v1/list_delete.go b/routes/api/v1/list_delete.go index f2e66e59e5..691bb9ab15 100644 --- a/routes/api/v1/list_delete.go +++ b/routes/api/v1/list_delete.go @@ -1,13 +1,38 @@ package v1 import ( - "github.com/labstack/echo" - "strconv" - "net/http" "git.kolaente.de/konrad/list/models" + "github.com/labstack/echo" + "net/http" + "strconv" ) func DeleteListByID(c echo.Context) error { + // swagger:operation DELETE /lists/{listID} lists deleteList + // --- + // summary: Deletes a list with all items on it + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: listID + // in: path + // description: ID of the list to delete + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Message" + // "400": + // "$ref": "#/responses/Message" + // "403": + // "$ref": "#/responses/Message" + // "404": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + // Check if we have our ID id := c.Param("id") // Make int @@ -36,4 +61,4 @@ func DeleteListByID(c echo.Context) error { } return c.JSON(http.StatusOK, models.Message{"The list was deleted with success."}) -} \ No newline at end of file +} diff --git a/routes/api/v1/list_item_add_update.go b/routes/api/v1/list_item_add_update.go index 8ddaae096a..bf7e973554 100644 --- a/routes/api/v1/list_item_add_update.go +++ b/routes/api/v1/list_item_add_update.go @@ -8,6 +8,33 @@ import ( ) func AddListItem(c echo.Context) error { + // swagger:operation PUT /lists/{listID} lists addListItem + // --- + // summary: Adds an item to a list + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: listID + // in: path + // description: ID of the list to use + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ListItem" + // responses: + // "200": + // "$ref": "#/responses/ListItem" + // "400": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + + // TODO: return 403 if you dont have the right to add an item to that list + // Get the list ID id := c.Param("id") // Make int @@ -20,6 +47,31 @@ func AddListItem(c echo.Context) error { } func UpdateListItem(c echo.Context) error { + // swagger:operation PUT /item/{itemID} lists updateListItem + // --- + // summary: Updates a list item + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: itemID + // in: path + // description: ID of the item to update + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ListItem" + // responses: + // "200": + // "$ref": "#/responses/ListItem" + // "400": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + // Get the item ID id := c.Param("id") // Make int @@ -73,4 +125,4 @@ func updateOrCreateListItemHelper(listID, itemID int64, c echo.Context) error { } return c.JSON(http.StatusOK, finalItem) -} \ No newline at end of file +} diff --git a/routes/api/v1/list_show.go b/routes/api/v1/list_show.go index 2cf7cb0b78..73ffc4b065 100644 --- a/routes/api/v1/list_show.go +++ b/routes/api/v1/list_show.go @@ -9,6 +9,27 @@ import ( // AddOrUpdateList Adds or updates a new list func GetListByID(c echo.Context) error { + // swagger:operation GET /lists/{listID} lists getList + // --- + // summary: gets one list with all todo items + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: listID + // in: path + // description: ID of the list to show + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/List" + // "400": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + // Check if we have our ID id := c.Param("id") // Make int diff --git a/routes/api/v1/lists_add_update.go b/routes/api/v1/lists_add_update.go index b14cab23f8..2148ad7362 100644 --- a/routes/api/v1/lists_add_update.go +++ b/routes/api/v1/lists_add_update.go @@ -7,8 +7,65 @@ import ( "strconv" ) +func AddList(c echo.Context) error { + // swagger:operation PUT /lists lists addList + // --- + // summary: Creates a new list owned by the currently logged in user + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/List" + // responses: + // "200": + // "$ref": "#/responses/List" + // "400": + // "$ref": "#/responses/Message" + // "403": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + + return addOrUpdateList(c) +} + +func UpdateList(c echo.Context) error { + // swagger:operation POST /lists/{listID} lists upadteList + // --- + // summary: Updates a list + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: listID + // in: path + // description: ID of the list to update + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/List" + // responses: + // "200": + // "$ref": "#/responses/List" + // "400": + // "$ref": "#/responses/Message" + // "403": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + + return addOrUpdateList(c) +} + // AddOrUpdateList Adds or updates a new list -func AddOrUpdateList(c echo.Context) error { +func addOrUpdateList(c echo.Context) error { // Get the list var list *models.List diff --git a/routes/api/v1/lists_list.go b/routes/api/v1/lists_list.go index 2789a8e557..95b1179859 100644 --- a/routes/api/v1/lists_list.go +++ b/routes/api/v1/lists_list.go @@ -8,6 +8,18 @@ import ( // GetListsByUser gets all lists a user owns func GetListsByUser(c echo.Context) error { + // swagger:operation GET /lists lists getLists + // --- + // summary: Gets all lists owned by the current user + // consumes: + // - application/json + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/List" + // "500": + // "$ref": "#/responses/Message" currentUser, err := models.GetCurrentUser(c) if err != nil { diff --git a/routes/api/v1/login.go b/routes/api/v1/login.go index 3237a764aa..fbc7644008 100644 --- a/routes/api/v1/login.go +++ b/routes/api/v1/login.go @@ -12,6 +12,26 @@ import ( // Login is the login handler func Login(c echo.Context) error { + // swagger:operation POST /login user login + // --- + // summary: Logs a user in. Returns a JWT-Token to authenticate requests + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserLogin" + // responses: + // "200": + // "$ref": "#/responses/Token" + // "400": + // "$ref": "#/responses/Message" + // "403": + // "$ref": "#/responses/Message" + u := new(models.UserLogin) if err := c.Bind(u); err != nil { return c.JSON(http.StatusBadRequest, models.Message{"Please provide a username and password."}) diff --git a/routes/api/v1/swagger/options.go b/routes/api/v1/swagger/options.go new file mode 100644 index 0000000000..6542f58afb --- /dev/null +++ b/routes/api/v1/swagger/options.go @@ -0,0 +1,22 @@ +package swagger + +import "git.kolaente.de/konrad/list/models" + +// not actually a response, just a hack to get go-swagger to include definitions +// of the various XYZOption structs + +// parameterBodies +// swagger:response parameterBodies +type swaggerParameterBodies struct { + // in:body + UserLogin models.UserLogin + + // in:body + ApiUserPassword models.ApiUserPassword + + // in:body + List models.List + + // in:body + ListItem models.ListItem +} diff --git a/routes/api/v1/swagger/responses.go b/routes/api/v1/swagger/responses.go new file mode 100644 index 0000000000..36cd0f8e9c --- /dev/null +++ b/routes/api/v1/swagger/responses.go @@ -0,0 +1,52 @@ +package swagger + +import "git.kolaente.de/konrad/list/models" + +// Message +// swagger:response Message +type swaggerResponseMessage struct { + // in:body + Body models.Message `json:"body"` +} + +// ================ +// User definitions +// ================ + +// User Object +// swagger:response User +type swaggerResponseUser struct { + // in:body + Body models.User `json:"body"` +} + +// Token +// swagger:response Token +type swaggerResponseToken struct { + // The body message + // in:body + Body struct { + // The token + // + // Required: true + Token string `json:"token"` + } `json:"body"` +} + +// ================ +// List definitions +// ================ + +// List +// swagger:response List +type swaggerResponseLIst struct { + // in:body + Body models.List `json:"body"` +} + +// ListItem +// swagger:response ListItem +type swaggerResponseLIstItem struct { + // in:body + Body models.ListItem `json:"body"` +} diff --git a/routes/api/v1/user_add_update.go b/routes/api/v1/user_add_update.go index 0e74452575..d531e4c3bc 100644 --- a/routes/api/v1/user_add_update.go +++ b/routes/api/v1/user_add_update.go @@ -1,36 +1,47 @@ package v1 import ( - "encoding/json" "git.kolaente.de/konrad/list/models" "github.com/labstack/echo" "net/http" "strconv" - "strings" ) -// UserAddOrUpdate is the handler to add a user -func UserAddOrUpdate(c echo.Context) error { +func RegisterUser(c echo.Context) error { + + // swagger:operation POST /register user register + // --- + // summary: Creates a new user account + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/ApiUserPassword" + // responses: + // "200": + // "$ref": "#/responses/User" + // "400": + // "$ref": "#/responses/Message" + // "500": + // "$ref": "#/responses/Message" + + return userAddOrUpdate(c) +} + +// userAddOrUpdate is the handler to add a user +func userAddOrUpdate(c echo.Context) error { // TODO: prevent everyone from updating users // Check for Request Content - userFromString := c.FormValue("user") - var datUser *models.User + var datUser *models.ApiUserPassword - if userFromString == "" { - // b := new(models.User) - if err := c.Bind(&datUser); err != nil { - return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."}) - } - } else { - // Decode the JSON - dec := json.NewDecoder(strings.NewReader(userFromString)) - err := dec.Decode(&datUser) - - if err != nil { - return c.JSON(http.StatusBadRequest, models.Message{"Error decoding user: " + err.Error()}) - } + if err := c.Bind(&datUser); err != nil { + return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."}) } // Check if we have an ID other than the one in the struct @@ -54,9 +65,9 @@ func UserAddOrUpdate(c echo.Context) error { // Insert or update the user var newUser models.User if exists { - newUser, err = models.UpdateUser(*datUser) + newUser, err = models.UpdateUser(datUser.APIFormat()) } else { - newUser, err = models.CreateUser(*datUser) + newUser, err = models.CreateUser(datUser.APIFormat()) } if err != nil { @@ -88,8 +99,5 @@ func UserAddOrUpdate(c echo.Context) error { return c.JSON(http.StatusInternalServerError, models.Message{"Error"}) } - // Obfuscate his password - newUser.Password = "" - return c.JSON(http.StatusOK, newUser) } diff --git a/routes/routes.go b/routes/routes.go index 4814a60591..8dfd33f572 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -1,3 +1,28 @@ +// Package v1 List API. +// +// This documentation describes the List API. +// +// Schemes: http, https +// BasePath: /api/v1 +// Version: 0.1 +// License: GPLv3 +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Security: +// - AuthorizationHeaderToken : +// +// SecurityDefinitions: +// AuthorizationHeaderToken: +// type: apiKey +// name: Authorization +// in: header +// +// swagger:meta package routes import ( @@ -6,6 +31,7 @@ import ( "git.kolaente.de/konrad/list/models" apiv1 "git.kolaente.de/konrad/list/routes/api/v1" + _ "git.kolaente.de/konrad/list/routes/api/v1/swagger" // for docs generation ) // NewEcho registers a new Echo instance @@ -23,6 +49,7 @@ func NewEcho() *echo.Echo { // RegisterRoutes registers all routes for the application func RegisterRoutes(e *echo.Echo) { + // Middleware for cors e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { res := c.Response() @@ -46,17 +73,17 @@ func RegisterRoutes(e *echo.Echo) { a.OPTIONS("/lists/:id", SetCORSHeader) a.POST("/login", apiv1.Login) - a.POST("/register", apiv1.UserAddOrUpdate) + a.POST("/register", apiv1.RegisterUser) // ===== Routes with Authetification ===== // Authetification a.Use(middleware.JWT(models.Config.JWTLoginSecret)) a.POST("/tokenTest", apiv1.CheckToken) - a.PUT("/lists", apiv1.AddOrUpdateList) a.GET("/lists", apiv1.GetListsByUser) + a.PUT("/lists", apiv1.AddList) a.GET("/lists/:id", apiv1.GetListByID) - a.POST("/lists/:id", apiv1.AddOrUpdateList) + a.POST("/lists/:id", apiv1.UpdateList) a.PUT("/lists/:id", apiv1.AddListItem) a.DELETE("/lists/:id", apiv1.DeleteListByID)