diff --git a/docker-compose.yml b/docker-compose.yml index c8d30db..cfdb882 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,4 +43,3 @@ services: profiles: ["tests"] depends_on: - auth - #command: ls -l diff --git a/src/db/accounts.go b/src/db/accounts.go index 7e82907..a953ba7 100644 --- a/src/db/accounts.go +++ b/src/db/accounts.go @@ -2,6 +2,7 @@ package db import ( "context" + "errors" "strings" "github.com/google/uuid" @@ -48,6 +49,36 @@ func (d Db) AccountCreate(input AccountCreateInput) (CreatedAccount, error) { }, nil } +func (d Db) AccountDel(accountID string) error { + d.Log.Info("Trying to delete account", "accountID", accountID) + + _, renewalTokensErr := d.DbPool.Exec(context.Background(), "DELETE FROM \"renewalTokens\" WHERE \"accountId\" = $1;", accountID) + if renewalTokensErr != nil { + d.Log.Error("Could not remove renewal tokens for account", "err", renewalTokensErr.Error(), "accountID", accountID) + return renewalTokensErr + } + + _, fieldsErr := d.DbPool.Exec(context.Background(), "DELETE FROM \"accountsFields\" WHERE \"accountId\" = $1;", accountID) + if fieldsErr != nil { + d.Log.Error("Could not remove account fields", "err", fieldsErr.Error(), "accountID", accountID) + return fieldsErr + } + + res, err := d.DbPool.Exec(context.Background(), "DELETE FROM accounts WHERE id = $1", accountID) + if err != nil { + d.Log.Error("Could not remove account", "err", err.Error(), "accountID", accountID) + return err + } + + if string(res) == "DELETE 0" { + d.Log.Info("Tried to delete account, but none exists", "accountID", accountID) + err := errors.New("No account found for given accountID") + return err + } + + return nil +} + // AccountGet fetches an account from the database func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, error) { d.Log.Debug("Trying to get account", "accountID", accountID, "len(APIKey)", len(APIKey)) diff --git a/src/docs/docs.go b/src/docs/docs.go index 3adaf36..26581f1 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -54,12 +54,105 @@ var doc = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/db.CreatedAccount" } }, + "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" + } + } + }, + "409": { + "description": "Conflict", + "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" + } + } + } + } + } + }, + "/account/:id": { + "delete": { + "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": "Delete an account", + "operationId": "account-del", + "parameters": [ + { + "type": "string", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, "401": { "description": "Unauthorized", "schema": { diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 8c6c1ee..9a11a6e 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -38,12 +38,105 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "$ref": "#/definitions/db.CreatedAccount" } }, + "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" + } + } + }, + "409": { + "description": "Conflict", + "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" + } + } + } + } + } + }, + "/account/:id": { + "delete": { + "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": "Delete an account", + "operationId": "account-del", + "parameters": [ + { + "type": "string", + "description": "Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ResJSONError" + } + } + }, "401": { "description": "Unauthorized", "schema": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 5310f06..955ef20 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -96,10 +96,75 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/db.CreatedAccount' + "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 + "409": + description: Conflict + 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: Create an account + /account/:id: + delete: + consumes: + - application/json + description: |- + Requires Authorization-header with role "admin". + Example: Authorization: bearer xxx + Where "xxx" is a valid JWT token + operationId: account-del + parameters: + - description: Account ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "400": + description: Bad Request + schema: + items: + $ref: '#/definitions/handlers.ResJSONError' + type: array "401": description: Unauthorized schema: @@ -124,7 +189,7 @@ paths: items: $ref: '#/definitions/handlers.ResJSONError' type: array - summary: Create an account + summary: Delete an account /account/{id}: get: consumes: diff --git a/src/handlers/delete.go b/src/handlers/delete.go new file mode 100644 index 0000000..6155dd2 --- /dev/null +++ b/src/handlers/delete.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +// AccountDel godoc +// @Summary Delete an account +// @Description Requires Authorization-header with role "admin". +// @Description Example: Authorization: bearer xxx +// @Description Where "xxx" is a valid JWT token +// @ID account-del +// @Accept json +// @Produce json +// @Param id path string true "Account ID" +// @Success 204 {string} string "" +// @Failure 400 {object} []ResJSONError +// @Failure 401 {object} []ResJSONError +// @Failure 403 {object} []ResJSONError +// @Failure 404 {object} []ResJSONError +// @Failure 415 {object} []ResJSONError +// @Failure 500 {object} []ResJSONError +// @Router /account/:id [delete] +func (h Handlers) AccountDel(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()}}) + } + + err := h.Db.AccountDel(accountID) + if err != nil { + if err.Error() == "No account found for given accountID" { + return c.Status(404).JSON([]ResJSONError{{Error: err.Error()}}) + } else { + return c.Status(500).JSON([]ResJSONError{{Error: "Database error when trying to remove account"}}) + } + } + + return c.Status(204).Send(nil) +} diff --git a/src/handlers/get.go b/src/handlers/get.go index fb08008..9bf41b0 100644 --- a/src/handlers/get.go +++ b/src/handlers/get.go @@ -21,7 +21,6 @@ import ( // @Router /account/{id} [get] func (h Handlers) AccountGet(c *fiber.Ctx) error { accountID := c.Params("accountID") - // logContext := log.WithFields(log.Fields{"accountID": accountID}) authErr := h.RequireAdminRoleOrAccountID(c, accountID) if authErr != nil { @@ -30,7 +29,11 @@ func (h Handlers) AccountGet(c *fiber.Ctx) error { account, accountErr := h.Db.AccountGet(accountID, "", "") if accountErr != nil { - return c.Status(500).JSON([]ResJSONError{{Error: accountErr.Error()}}) + if accountErr.Error() == "no rows in result set" { + return c.Status(404).JSON([]ResJSONError{{Error: "No account found for given accountID"}}) + } else { + return c.Status(500).JSON([]ResJSONError{{Error: accountErr.Error()}}) + } } return c.JSON(account) diff --git a/src/handlers/helpers.go b/src/handlers/helpers.go index 7a593ac..1800dd5 100644 --- a/src/handlers/helpers.go +++ b/src/handlers/helpers.go @@ -73,7 +73,9 @@ func (h Handlers) parseHeaders(c *fiber.Ctx) map[string]string { lineParts := strings.Split(line, ": ") if len(lineParts) == 1 { - h.Log.Debug("Ignoring header line", "line", line) + if len(line) != 0 { + h.Log.Debug("Ignoring header line", "line", line) + } } else { headersMap[lineParts[0]] = lineParts[1] } diff --git a/src/main.go b/src/main.go index 1394458..f8ffb22 100644 --- a/src/main.go +++ b/src/main.go @@ -91,6 +91,7 @@ func main() { app.Get("/swagger", func(c *fiber.Ctx) error { return c.Redirect("/swagger/index.html") }) app.Get("/swagger/*", swagger.Handler) + app.Delete("/account/:accountID", handlers.AccountDel) app.Get("/account/:accountID", handlers.AccountGet) app.Post("/account", handlers.AccountCreate) app.Post("/auth/api-key", handlers.AccountAuthAPIKey) diff --git a/tests/test-cases/00start.js b/tests/test-cases/00start.js index 315a7cb..1ceb81e 100644 --- a/tests/test-cases/00start.js +++ b/tests/test-cases/00start.js @@ -9,6 +9,4 @@ test('test-cases/00start.js: Wait for auth API to be ready', async t => { const backendHealthCheck = await got(process.env.AUTH_URL, { retry: 2000 }); t.equal(backendHealthCheck.statusCode, 200, 'Auth API should answer with status code 200'); - - t.end(); }); diff --git a/tests/test-cases/01basic.js b/tests/test-cases/01basic.js index 7c9f861..de806a6 100644 --- a/tests/test-cases/01basic.js +++ b/tests/test-cases/01basic.js @@ -3,9 +3,15 @@ import jwt from 'jsonwebtoken' import setConfig from '../test-helpers/config.js'; import test from 'tape'; -test('test-cases/01basic.js: Basic stuff', async t => { - t.comment('Authing with configurated API KEY'); +let adminJWT; +let adminJWTString; +let user; +let userJWT; +let userJWTString; +const userName = 'test-tomte nöff #18'; +const password = 'lurpassare7½TUR'; +test('test-cases/01basic.js: Authing with configurated API KEY', async t => { // Wrong API key try { await got.post(`${process.env.AUTH_URL}/auth/api-key`, { @@ -18,18 +24,20 @@ test('test-cases/01basic.js: Basic stuff', async t => { t.equal(err.message, 'Response code 403 (Forbidden)', 'Calling /auth/api-key with wrong api-key should result in a 403') } + // Successful auth const authRes = await got.post(`${process.env.AUTH_URL}/auth/api-key`, { json: 'hihi', responseType: 'json', }); t.notEqual(authRes.body.jwt, undefined, 'The body should include a jwt key'); t.notEqual(authRes.body.renewalToken, undefined, 'The body should include a renewalToken'); + adminJWTString = authRes.body.jwt; - const adminJWT = jwt.verify(authRes.body.jwt, process.env.JWT_SHARED_SECRET); + adminJWT = jwt.verify(adminJWTString, process.env.JWT_SHARED_SECRET); t.equal(adminJWT.accountName, 'admin', 'The verified account name should be "admin"'); +}); - t.comment('GETting the admin account, with the token we just obtained'); - +test('test-cases/01basic.js: GETting the admin account, with the token we just obtained', async t => { try { await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`); t.fail('Calling /account/{id} without proper auth token should give 403'); @@ -38,11 +46,99 @@ test('test-cases/01basic.js: Basic stuff', async t => { } const accountRes = await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`, { - headers: { 'Authorization': `bearer ${authRes.body.jwt}`}, + headers: { 'Authorization': `bearer ${adminJWTString}`}, responseType: 'json', }); t.equal(adminJWT.accountId, accountRes.body.id, 'The account ids should match'); - - t.end(); +}); + +test('test-cases/01basic.js: Creating a new account', async t => { + const res = await got.post(`${process.env.AUTH_URL}/account`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + json: { + fields: [ + { + name: 'nördområde', + values: ['tåg', 'trädgårdstomtar'], + }, + { + name: 'role', + values: ['user'], + } + ], + name: userName, + password, + }, + responseType: 'json', + }); + + user = res.body; + + t.notEqual(user.id, undefined, 'The new account should have an id'); + t.notEqual(user.apiKey, undefined, 'The new account should have an apiKey'); + + try { + await got.post(`${process.env.AUTH_URL}/account`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + json: { + fields: [{name: 'role',values: ['user'],}], + name: userName, + password, + }, + responseType: 'json', + }); + t.fail('Trying to create another account with the same name should fail with a 409'); + } catch(err) { + t.equal(err.message, 'Response code 409 (Conflict)', 'Trying to create another account with the same name should fail with a 409'); + } +}); + +test('test-cases/01basic.js: Auth by username and password', async t => { + const authRes = await got.post(`${process.env.AUTH_URL}/auth/password`, { + json: { + name: userName, + password, + }, + responseType: 'json', + }); + t.notEqual(authRes.body.jwt, undefined, 'The body should include a jwt key'); + t.notEqual(authRes.body.renewalToken, undefined, 'The body should include a renewalToken'); + userJWTString = authRes.body.jwt; + + userJWT = jwt.verify(userJWTString, process.env.JWT_SHARED_SECRET); + t.equal(userJWT.accountName, userName, 'The verified account name should match the created user'); +}); + +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 + await got.delete(`${process.env.AUTH_URL}/account/a423e690-74b9-4f37-9976-f5bf75a5ea32`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + responseType: 'json', + retry: 0, + }); + t.fail('Response status for DELETing an account that does not exist should be 404'); + } catch (err) { + t.equal(err.message, 'Response code 404 (Not Found)', 'Response status for DELETing an account that does not exist should be 404'); + } + + const delRes = await got.delete(`${process.env.AUTH_URL}/account/${user.id}`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + responseType: 'json', + retry: 0, + }); + + t.equal(delRes.statusCode, 204, 'Response status for DELETE should be 204'); + + try { + await got(`${process.env.AUTH_URL}/account/${user.id}`, { + headers: { 'Authorization': `bearer ${adminJWTString}`}, + responseType: 'json', + retry: 0, + }); + t.fail('Response status for GETing the account again should be 404'); + } catch (err) { + t.equal(err.message, 'Response code 404 (Not Found)', 'Response status for GETing the account again should be 404'); + } });