From db6695e7565adc012ab08d1757f91828a4164f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Gulczy=C5=84ski?= Date: Wed, 23 Aug 2023 16:06:12 +0200 Subject: [PATCH] restrict login to employers with verified emails (#149) --- README.md | 24 ++++++++++--- docs/docs.go | 67 ++++++++++++++++++++++------------- docs/swagger.json | 67 ++++++++++++++++++++++------------- docs/swagger.yaml | 47 +++++++++++++++--------- internal/api/employer.go | 11 ++++-- internal/api/employer_test.go | 21 +++++++++++ internal/api/user.go | 10 ++++-- 7 files changed, 174 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 62efef9..6c024be 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,15 @@ Search functionality is implemented both with Postgres and Elasticsearch (depend ### The app uses: - Postgres - Docker -- [Gin](https://github.com/gin-gonic/gin) +- Redis - [Elasticsearch](https://github.com/elastic/go-elasticsearch) +- [Gin](https://github.com/gin-gonic/gin) - [golang-migrate](https://github.com/golang-migrate/migrate) - [sqlc](https://github.com/kyleconroy/sqlc) +- [asynq](https://github.com/hibiken/asynq) - [testify](https://github.com/stretchr/testify) - [PASETO Security Tokens](https://github.com/o1egl/paseto) - [Viper](https://github.com/spf13/viper) -- [jordan-wright/email](https://github.com/jordan-wright/email) - [gin-swagger](https://github.com/swaggo/gin-swagger)
@@ -55,7 +56,7 @@ This API provides a set of endpoints for managing: - jobs - job applications -(and indirectly: user skills and job skills) +(and indirectly: user skills, job skills and verify emails table) After running the server, the Swagger documentation is available at http://localhost:8080/swagger/index.html. @@ -77,12 +78,19 @@ in JSON format. On success, the response has a `201 Created` status code and ret user in JSON format. If the request body is invalid, a `400 Bad Request` status code is returned. If a user with the given email already exists, a `403 Forbidden` status code is returned. In case of any other error, a `500 Internal Server Error` status code is returned. +After registering, a verification email is sent to the provided email address. + ++ `GET /users/verify-email`: This endpoint verifies a user’s email by providing a verify email ID and +secret code that should be sent to the user in the verification email. The request body must contain the verify +email ID and secret code as query parameters. On success, the response has a `200 OK` status code and returns the +verification result in JSON format. If the request query is invalid, a `400 Bad Request` status code is returned. +In case of any other error, a `500 Internal Server Error` status code is returned. + `POST /users/login`: This endpoint logs in a user. The request body must contain the user credentials (email, password) in JSON format. On success, the response has a `200 OK` status code and returns an access token and the authenticated user in JSON format. If the request body is invalid, a `400 Bad Request` status code is returned. If the password is incorrect, a `401 Unauthorized` -status code is returned. If a user with the given email does not exist, a `404 Not Found` status +status code is returned. If user has not verified email, `403 Forbidden` is returned. If a user with the given email does not exist, a `404 Not Found` status code is returned. In case of any other error, a `500 Internal Server Error` status code is returned. + `GET /users`: This endpoint retrieves the details of the logged-in user. On success, the response @@ -126,12 +134,20 @@ the created employer in JSON format. If the request body is invalid, a `400` status code is returned. If a company with the given name or an employer with the given email already exists, a `403 Forbidden` status code is returned. In case of any other error, a 500 Internal Error status code is returned. +After registering, a verification email is sent to the provided email address. + ++ `GET /employers/verify-email`: This endpoint verifies an employer’s email by providing a verify email ID and +secret code that should be sent to the user in the verification email. The request body must contain the verify +email ID and secret code as query parameters. On success, the response has a `200 OK` status code and returns the +verification result in JSON format. If the request query is invalid, a `400 Bad Request` status code is returned. +In case of any other error, a `500 Internal Server Error` status code is returned. + `POST /employers/login`: This endpoint logs in an employer. The request body must contain the employer credentials (email, password) in JSON format. On success, the response has a `200 OK` status code and returns an access token and the authenticated employer in JSON format. If the request body is invalid, a `400 Bad Request` status code is returned. If the password is incorrect, a `401 Unauthorized` status code is returned. +If the emails is not verified, a `403 Forbidden` is returned. If an employer with the given email or a company with the given id does not exist, a `404 Not Found` status code is returned. In case of any other error, a `500 Internal Server Error` status code is returned. diff --git a/docs/docs.go b/docs/docs.go index 697ec97..e893590 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -237,6 +237,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.ErrorResponse" } }, + "403": { + "description": "Email not verified", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, "404": { "description": "Employer with given email or company with given id does not exist", "schema": { @@ -367,13 +373,18 @@ const docTemplate = `{ "summary": "Verify employer email", "parameters": [ { - "description": "Verify email ID and secret code from the email.", - "name": "VerifyEmployerEmailRequest", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.verifyEmployerEmailRequest" - } + "minLength": 32, + "type": "string", + "name": "code", + "in": "query", + "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "id", + "in": "query", + "required": true } ], "responses": { @@ -1755,6 +1766,12 @@ const docTemplate = `{ "$ref": "#/definitions/api.ErrorResponse" } }, + "403": { + "description": "Email not verified", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, "404": { "description": "User with given email does not exist", "schema": { @@ -1830,10 +1847,29 @@ const docTemplate = `{ "/users/verify-email": { "get": { "description": "Verify user email by providing verify email ID and secret code that should be sent to the user in the verification email.", + "produces": [ + "application/json" + ], "tags": [ "users" ], "summary": "Verify user email", + "parameters": [ + { + "minLength": 32, + "type": "string", + "name": "code", + "in": "query", + "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "id", + "in": "query", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -2442,23 +2478,6 @@ const docTemplate = `{ } } }, - "api.verifyEmployerEmailRequest": { - "type": "object", - "required": [ - "code", - "id" - ], - "properties": { - "code": { - "type": "string", - "minLength": 32 - }, - "id": { - "type": "integer", - "minimum": 1 - } - } - }, "api.verifyEmployerEmailResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index d6c205d..0e94c95 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -225,6 +225,12 @@ "$ref": "#/definitions/api.ErrorResponse" } }, + "403": { + "description": "Email not verified", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, "404": { "description": "Employer with given email or company with given id does not exist", "schema": { @@ -355,13 +361,18 @@ "summary": "Verify employer email", "parameters": [ { - "description": "Verify email ID and secret code from the email.", - "name": "VerifyEmployerEmailRequest", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api.verifyEmployerEmailRequest" - } + "minLength": 32, + "type": "string", + "name": "code", + "in": "query", + "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "id", + "in": "query", + "required": true } ], "responses": { @@ -1743,6 +1754,12 @@ "$ref": "#/definitions/api.ErrorResponse" } }, + "403": { + "description": "Email not verified", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, "404": { "description": "User with given email does not exist", "schema": { @@ -1818,10 +1835,29 @@ "/users/verify-email": { "get": { "description": "Verify user email by providing verify email ID and secret code that should be sent to the user in the verification email.", + "produces": [ + "application/json" + ], "tags": [ "users" ], "summary": "Verify user email", + "parameters": [ + { + "minLength": 32, + "type": "string", + "name": "code", + "in": "query", + "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "id", + "in": "query", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -2430,23 +2466,6 @@ } } }, - "api.verifyEmployerEmailRequest": { - "type": "object", - "required": [ - "code", - "id" - ], - "properties": { - "code": { - "type": "string", - "minLength": 32 - }, - "id": { - "type": "integer", - "minimum": 1 - } - } - }, "api.verifyEmployerEmailResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 57dd42c..03047ed 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -391,18 +391,6 @@ definitions: skills_description: type: string type: object - api.verifyEmployerEmailRequest: - properties: - code: - minLength: 32 - type: string - id: - minimum: 1 - type: integer - required: - - code - - id - type: object api.verifyEmployerEmailResponse: properties: message: @@ -747,6 +735,10 @@ paths: description: Incorrect password schema: $ref: '#/definitions/api.ErrorResponse' + "403": + description: Email not verified + schema: + $ref: '#/definitions/api.ErrorResponse' "404": description: Employer with given email or company with given id does not exist @@ -830,12 +822,16 @@ paths: description: Verify employer email by providing verify email ID and secret code that should be sent to the user in the verification email. parameters: - - description: Verify email ID and secret code from the email. - in: body - name: VerifyEmployerEmailRequest + - in: query + minLength: 32 + name: code required: true - schema: - $ref: '#/definitions/api.verifyEmployerEmailRequest' + type: string + - in: query + minimum: 1 + name: id + required: true + type: integer produces: - application/json responses: @@ -1755,6 +1751,10 @@ paths: description: Incorrect password schema: $ref: '#/definitions/api.ErrorResponse' + "403": + description: Email not verified + schema: + $ref: '#/definitions/api.ErrorResponse' "404": description: User with given email does not exist schema: @@ -1807,6 +1807,19 @@ paths: get: description: Verify user email by providing verify email ID and secret code that should be sent to the user in the verification email. + parameters: + - in: query + minLength: 32 + name: code + required: true + type: string + - in: query + minimum: 1 + name: id + required: true + type: integer + produces: + - application/json responses: "200": description: OK diff --git a/internal/api/employer.go b/internal/api/employer.go index fcb1cad..843180e 100644 --- a/internal/api/employer.go +++ b/internal/api/employer.go @@ -158,8 +158,9 @@ type loginEmployerResponse struct { // @param LoginEmployerRequest body loginEmployerRequest true "Employer credentials" // @Success 200 {object} loginEmployerResponse // @Failure 400 {object} ErrorResponse "Invalid request body" -// @Failure 404 {object} ErrorResponse "Employer with given email or company with given id does not exist" // @Failure 401 {object} ErrorResponse "Incorrect password" +// @Failure 403 {object} ErrorResponse "Email not verified" +// @Failure 404 {object} ErrorResponse "Employer with given email or company with given id does not exist" // @Failure 500 {object} ErrorResponse "Any other error" // @Router /employers/login [post] // loginEmployer handles login of an employer @@ -182,6 +183,12 @@ func (server *Server) loginEmployer(ctx *gin.Context) { return } + // check if the user has verified email + if !employer.IsEmailVerified { + ctx.JSON(http.StatusForbidden, errorResponse(emailNotVerifiedErr)) + return + } + // check password err = utils.CheckPassword(request.Password, employer.HashedPassword) if err != nil { @@ -587,7 +594,7 @@ type verifyEmployerEmailResponse struct { // @Description Verify employer email by providing verify email ID and secret code that should be sent to the user in the verification email. // @Tags employers // @Produce json -// @param VerifyEmployerEmailRequest body verifyEmployerEmailRequest true "Verify email ID and secret code from the email." +// @param VerifyEmployerEmailRequest query verifyEmployerEmailRequest true "Verify email ID and secret code from the email." // @Success 200 {object} verifyEmployerEmailResponse // @Failure 400 {object} ErrorResponse "Invalid request body." // @Failure 500 {object} ErrorResponse "Any other error." diff --git a/internal/api/employer_test.go b/internal/api/employer_test.go index 13076f8..0c5d016 100644 --- a/internal/api/employer_test.go +++ b/internal/api/employer_test.go @@ -249,6 +249,7 @@ func TestCreateEmployerAPI(t *testing.T) { func TestLoginEmployerAPI(t *testing.T) { employer, password, company := generateRandomEmployerAndCompany(t) + employer.IsEmailVerified = true testCases := []struct { name string @@ -409,6 +410,26 @@ func TestLoginEmployerAPI(t *testing.T) { require.Equal(t, http.StatusBadRequest, recorder.Code) }, }, + { + name: "Email Not Verified", + body: gin.H{ + "email": employer.Email, + "password": password, + }, + buildStubs: func(store *mockdb.MockStore) { + employer.IsEmailVerified = false + store.EXPECT(). + GetEmployerByEmail(gomock.Any(), gomock.Eq(employer.Email)). + Times(1). + Return(employer, nil) + store.EXPECT(). + GetCompanyByID(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusForbidden, recorder.Code) + }, + }, } for i := range testCases { tc := testCases[i] diff --git a/internal/api/user.go b/internal/api/user.go index 27a442a..5f2c2ca 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -17,6 +17,10 @@ import ( "time" ) +var ( + emailNotVerifiedErr = errors.New("email not verified. Please verify your email before logging in") +) + type Skill struct { ID int32 `json:"id"` SkillName string `json:"skill"` @@ -198,6 +202,7 @@ type loginUserResponse struct { // @Success 200 {object} loginUserResponse // @Failure 400 {object} ErrorResponse "Invalid request body" // @Failure 401 {object} ErrorResponse "Incorrect password" +// @Failure 403 {object} ErrorResponse "Email not verified" // @Failure 404 {object} ErrorResponse "User with given email does not exist" // @Failure 500 {object} ErrorResponse "Any other error" // @Router /users/login [post] @@ -223,8 +228,7 @@ func (server *Server) loginUser(ctx *gin.Context) { // check if the user has verified email if !user.IsEmailVerified { - err = fmt.Errorf("email not verified. Please verify your email before logging in") - ctx.JSON(http.StatusForbidden, errorResponse(err)) + ctx.JSON(http.StatusForbidden, errorResponse(emailNotVerifiedErr)) return } @@ -569,6 +573,8 @@ type verifyUserEmailResponse struct { // @Summary Verify user email // @Description Verify user email by providing verify email ID and secret code that should be sent to the user in the verification email. // @Tags users +// @Param VerifyUserEmailRequest query verifyUserEmailRequest true "Verify user email request" +// @Produce json // @Success 200 {object} verifyUserEmailResponse // @Failure 400 {object} ErrorResponse "Invalid request body." // @Failure 500 {object} ErrorResponse "Any other error."