From 880a384f35da0e97c9833146f4725e46595efcb3 Mon Sep 17 00:00:00 2001 From: Lilleman Date: Thu, 24 Jun 2021 01:55:47 +0200 Subject: [PATCH] Added PUT /account/{id}/fields --- src/db/accounts.go | 56 +++++++++++++++++++++-- src/docs/docs.go | 91 ++++++++++++++++++++++++++++++++++++- src/docs/swagger.json | 91 ++++++++++++++++++++++++++++++++++++- src/docs/swagger.yaml | 64 +++++++++++++++++++++++++- src/handlers/delete.go | 2 +- src/handlers/put.go | 52 +++++++++++++++++++++ src/main.go | 2 + tests/test-cases/01basic.js | 26 +++++++++++ 8 files changed, 376 insertions(+), 8 deletions(-) create mode 100644 src/handlers/put.go diff --git a/src/db/accounts.go b/src/db/accounts.go index a953ba7..cebfc54 100644 --- a/src/db/accounts.go +++ b/src/db/accounts.go @@ -34,12 +34,12 @@ func (d Db) AccountCreate(input AccountCreateInput) (CreatedAccount, error) { _, err := d.DbPool.Exec(context.Background(), accountFieldsSQL, newFieldID, input.ID, field.Name, field.Values) if err != nil { - if strings.HasPrefix(err.Error(), "ERROR: duplicate key") { - d.Log.Error("Database error when trying to account field", "err", err.Error()) - } + //if strings.HasPrefix(err.Error(), "ERROR: duplicate key") { + d.Log.Error("Database error when trying to add account field", "err", err.Error(), "accountID", input.ID, "fieldName", field.Name, "fieldvalues", field.Values) + // } } - d.Log.Debug("Added account field", "accountId", input.ID, "fieldName", field.Name, "fieldValues", field.Values) + d.Log.Debug("Added account field", "accountID", input.ID, "fieldName", field.Name, "fieldValues", field.Values) } return CreatedAccount{ @@ -129,3 +129,51 @@ func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, e return account, nil } + +func (d Db) AccountUpdateFields(accountID string, fields []AccountCreateInputFields) (Account, error) { + // Begin database transaction + conn, err := d.DbPool.Acquire(context.Background()) + if err != nil { + d.Log.Error("Could not acquire database connection", "err", err.Error(), "accountID", accountID) + return Account{}, err + } + + tx, err := conn.Begin(context.Background()) + if err != nil { + d.Log.Error("Could not begin database transaction", "err", err.Error(), "accountID", accountID) + return Account{}, err + } + + // Rollback is safe to call even if the tx is already closed, so if + // the tx commits successfully, this is a no-op + defer tx.Rollback(context.Background()) + + _, err = tx.Exec(context.Background(), "DELETE FROM \"accountsFields\" WHERE \"accountId\" = $1;", accountID) + if err != nil { + d.Log.Error("Could not delete previous fields", "err", err.Error(), "accountID", accountID) + return Account{}, err + } + + accountFieldsSQL := "INSERT INTO \"accountsFields\" (id, \"accountId\", name, value) VALUES($1,$2,$3,$4);" + for _, field := range fields { + newFieldID, err := uuid.NewRandom() + if err != nil { + d.Log.Fatal("Could not create new Uuid", "err", err.Error()) + } + + _, err = tx.Exec(context.Background(), accountFieldsSQL, newFieldID, accountID, field.Name, field.Values) + if err != nil { + d.Log.Error("Database error when trying to add account field", "err", err.Error(), "accountID", accountID, "fieldName", field.Name, "fieldvalues", field.Values) + } + + d.Log.Debug("Added account field", "accountID", accountID, "fieldName", field.Name, "fieldValues", field.Values) + } + + err = tx.Commit(context.Background()) + if err != nil { + d.Log.Error("Database error when tying to commit", "err", err.Error()) + return Account{}, err + } + + return d.AccountGet(accountID, "", "") +} diff --git a/src/docs/docs.go b/src/docs/docs.go index 26581f1..e578b5f 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -119,7 +119,7 @@ var doc = `{ }, "/account/:id": { "delete": { - "description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", + "description": "Requires Authorization-header with role \"admin\" or a matching account id\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", "consumes": [ "application/json" ], @@ -171,6 +171,15 @@ var doc = `{ } } }, + "404": { + "description": "Not Found", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, "415": { "description": "Unsupported Media Type", "schema": { @@ -258,6 +267,86 @@ var doc = `{ } } }, + "/account/{id}/fields": { + "put": { + "description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update account fields", + "operationId": "account-update-fields", + "parameters": [ + { + "description": "Fields array with objects to be written to database", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/db.AccountCreateInputFields" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/db.Account" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "415": { + "description": "Unsupported Media Type", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + } + } + } + }, "/auth/api-key": { "post": { "description": "Authenticate account by API Key", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 9a11a6e..de32ca1 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -103,7 +103,7 @@ }, "/account/:id": { "delete": { - "description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", + "description": "Requires Authorization-header with role \"admin\" or a matching account id\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", "consumes": [ "application/json" ], @@ -155,6 +155,15 @@ } } }, + "404": { + "description": "Not Found", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, "415": { "description": "Unsupported Media Type", "schema": { @@ -242,6 +251,86 @@ } } }, + "/account/{id}/fields": { + "put": { + "description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update account fields", + "operationId": "account-update-fields", + "parameters": [ + { + "description": "Fields array with objects to be written to database", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/db.AccountCreateInputFields" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/db.Account" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "415": { + "description": "Unsupported Media Type", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + } + } + } + }, "/auth/api-key": { "post": { "description": "Authenticate account by API Key", diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 955ef20..46d3d8d 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -142,7 +142,7 @@ paths: consumes: - application/json description: |- - Requires Authorization-header with role "admin". + Requires Authorization-header with role "admin" or a matching account id Example: Authorization: bearer xxx Where "xxx" is a valid JWT token operationId: account-del @@ -177,6 +177,12 @@ paths: items: $ref: '#/definitions/handlers.ResJSONError' type: array + "404": + description: Not Found + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array "415": description: Unsupported Media Type schema: @@ -237,6 +243,62 @@ paths: $ref: '#/definitions/handlers.ResJSONError' type: array summary: Get account by id + /account/{id}/fields: + put: + consumes: + - application/json + description: |- + Requires Authorization-header with role "admin". + Example: Authorization: bearer xxx + Where "xxx" is a valid JWT token + operationId: account-update-fields + parameters: + - description: Fields array with objects to be written to database + in: body + name: body + required: true + schema: + items: + $ref: '#/definitions/db.AccountCreateInputFields' + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/db.Account' + "400": + description: Bad Request + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array + "401": + description: Unauthorized + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array + "403": + description: Forbidden + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array + "415": + description: Unsupported Media Type + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array + "500": + description: Internal Server Error + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array + summary: Update account fields /auth/api-key: post: consumes: diff --git a/src/handlers/delete.go b/src/handlers/delete.go index 8949299..29eca57 100644 --- a/src/handlers/delete.go +++ b/src/handlers/delete.go @@ -7,7 +7,7 @@ import ( // AccountDel godoc // @Summary Delete an account -// @Description Requires Authorization-header with role "admin". +// @Description Requires Authorization-header with role "admin" or a matching account id // @Description Example: Authorization: bearer xxx // @Description Where "xxx" is a valid JWT token // @ID account-del diff --git a/src/handlers/put.go b/src/handlers/put.go new file mode 100644 index 0000000..90cac0b --- /dev/null +++ b/src/handlers/put.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "gitlab.larvit.se/power-plan/auth/src/db" +) + +// AccountUpdateFields godoc +// @Summary Update account fields +// @Description Requires Authorization-header with role "admin". +// @Description Example: Authorization: bearer xxx +// @Description Where "xxx" is a valid JWT token +// @ID account-update-fields +// @Accept json +// @Produce json +// @Param body body []db.AccountCreateInputFields true "Fields array with objects to be written to database" +// @Success 200 {object} db.Account +// @Failure 400 {object} []ResJSONError +// @Failure 401 {object} []ResJSONError +// @Failure 403 {object} []ResJSONError +// @Failure 415 {object} []ResJSONError +// @Failure 500 {object} []ResJSONError +// @Router /account/{id}/fields [put] +func (h Handlers) AccountUpdateFields(c *fiber.Ctx) error { + accountID := c.Params("accountID") + + _, uuidErr := uuid.Parse(accountID) + if uuidErr != nil { + return c.Status(400).JSON([]ResJSONError{{Error: "Invalid uuid format"}}) + } + + authErr := h.RequireAdminRole(c) + if authErr != nil { + return c.Status(403).JSON([]ResJSONError{{Error: authErr.Error()}}) + } + + fieldsInput := new([]db.AccountCreateInputFields) + + if err := c.BodyParser(fieldsInput); err != nil { + return c.Status(400).JSON([]ResJSONError{ + {Error: err.Error()}, + }) + } + + updatedAccount, err := h.Db.AccountUpdateFields(accountID, *fieldsInput) + if err != nil { + return c.Status(500).JSON([]ResJSONError{{Error: "Internal server error"}}) + } + + return c.Status(200).JSON(updatedAccount) +} diff --git a/src/main.go b/src/main.go index f8ffb22..96e1958 100644 --- a/src/main.go +++ b/src/main.go @@ -97,6 +97,8 @@ func main() { app.Post("/auth/api-key", handlers.AccountAuthAPIKey) app.Post("/auth/password", handlers.AccountAuthPassword) app.Post("/renew-token", handlers.RenewToken) + app.Put("/account/:accountID/fields", handlers.AccountUpdateFields) + // app.Put("") log.Info("Trying to start web server", "WEB_BIND_HOST", os.Getenv("WEB_BIND_HOST")) diff --git a/tests/test-cases/01basic.js b/tests/test-cases/01basic.js index de806a6..408e3d2 100644 --- a/tests/test-cases/01basic.js +++ b/tests/test-cases/01basic.js @@ -110,6 +110,32 @@ test('test-cases/01basic.js: Auth by username and password', async t => { t.equal(userJWT.accountName, userName, 'The verified account name should match the created user'); }); +test('test-cases/01basic.js: PUT /account/{id}/fields', async t => { + const res = await got.put(`${process.env.AUTH_URL}/account/${user.id}/fields`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + json: [ + { + name: 'foo', + values: ['bar'], + }, + { + name: 'role', + values: ['tomte'], + } + ], + responseType: 'json', + }); + + t.equal(user.id, res.body.id, 'The responded account id should be the same as the old one'); + t.equal(Object.keys(res.body.fields).length, 2, 'There should only be two fields in total'); + t.equal(JSON.stringify(res.body.fields.foo), '["bar"]', 'The foo field should have values ["bar"]'); + t.equal(JSON.stringify(res.body.fields.role), '["tomte"]', 'The role field should have values ["tomte"]'); + + // Overload the previous user + user.fields = res.body.fields; + user.name = res.body.name; +}); + test('test-cases/01basic.js: Remove an account', async t => { try { // Random uuid that should not exist in the db. The chance of this existing is... small