From 408331a0965d3c8bb44d2ac9a8f281a5f704b136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Gulczy=C5=84ski?= Date: Wed, 23 Aug 2023 14:09:50 +0200 Subject: [PATCH] implement verifying employer emails (#147) --- Makefile | 3 +- docs/docs.go | 68 ++++++++++ docs/swagger.json | 68 ++++++++++ docs/swagger.yaml | 46 +++++++ internal/api/employer.go | 41 ++++++ internal/api/employer_test.go | 124 ++++++++++++++++++ internal/api/server.go | 1 + internal/api/user.go | 2 +- internal/api/user_test.go | 12 +- .../000007_add_verify_emails.up.sql | 2 +- .../000008_add_employer_verify_email.down.sql | 1 + .../000008_add_employer_verify_email.up.sql | 1 + internal/db/mock/store.go | 44 ++++++- internal/db/queries/employer.sql | 6 + internal/db/sqlc/company.sql.go | 2 +- internal/db/sqlc/db.go | 2 +- internal/db/sqlc/employer.sql.go | 36 ++++- internal/db/sqlc/job.sql.go | 2 +- internal/db/sqlc/job_application.sql.go | 2 +- internal/db/sqlc/job_skill.sql.go | 2 +- internal/db/sqlc/models.go | 19 +-- internal/db/sqlc/querier.go | 3 +- internal/db/sqlc/store.go | 3 +- internal/db/sqlc/tx_verify_email.go | 35 ++++- internal/db/sqlc/tx_verify_email_test.go | 25 +++- internal/db/sqlc/user.sql.go | 2 +- internal/db/sqlc/user_skill.sql.go | 2 +- internal/db/sqlc/verify_email.sql.go | 2 +- internal/db/sqlc/verify_email_test.go | 26 +++- .../worker/task_send_verification_email.go | 28 +++- 30 files changed, 555 insertions(+), 55 deletions(-) create mode 100644 internal/db/migrations/000008_add_employer_verify_email.down.sql create mode 100644 internal/db/migrations/000008_add_employer_verify_email.up.sql diff --git a/Makefile b/Makefile index 834fa92..ddba677 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,9 @@ migrate_down: migrate -path internal/db/migrations -database "postgresql://devuser:admin@localhost:5432/go_gin_job_search_db?sslmode=disable" -verbose down # generate db related go code with sqlc +# for windows: cmd.exe /c "docker run --rm -v ${PWD}:/src -w /src kjconroy/sqlc generate" sqlc: - cmd.exe /c "docker run --rm -v ${PWD}:/src -w /src kjconroy/sqlc generate" + sqlc generate # generate mock db for testing mock: diff --git a/docs/docs.go b/docs/docs.go index 7512165..697ec97 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -355,6 +355,49 @@ const docTemplate = `{ } } }, + "/employers/verify-email": { + "get": { + "description": "Verify employer email by providing verify email ID and secret code that should be sent to the user in the verification email.", + "produces": [ + "application/json" + ], + "tags": [ + "employers" + ], + "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" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.verifyEmployerEmailResponse" + } + }, + "400": { + "description": "Invalid request body.", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "500": { + "description": "Any other error.", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, "/job-applications": { "post": { "security": [ @@ -2399,6 +2442,31 @@ 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": { + "message": { + "type": "string" + } + } + }, "api.verifyUserEmailResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index c035863..d6c205d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -343,6 +343,49 @@ } } }, + "/employers/verify-email": { + "get": { + "description": "Verify employer email by providing verify email ID and secret code that should be sent to the user in the verification email.", + "produces": [ + "application/json" + ], + "tags": [ + "employers" + ], + "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" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.verifyEmployerEmailResponse" + } + }, + "400": { + "description": "Invalid request body.", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "500": { + "description": "Any other error.", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, "/job-applications": { "post": { "security": [ @@ -2387,6 +2430,31 @@ } } }, + "api.verifyEmployerEmailRequest": { + "type": "object", + "required": [ + "code", + "id" + ], + "properties": { + "code": { + "type": "string", + "minLength": 32 + }, + "id": { + "type": "integer", + "minimum": 1 + } + } + }, + "api.verifyEmployerEmailResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, "api.verifyUserEmailResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f377445..57dd42c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -391,6 +391,23 @@ 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: + type: string + type: object api.verifyUserEmailResponse: properties: message: @@ -808,6 +825,35 @@ paths: summary: Get user as employer tags: - employers + /employers/verify-email: + get: + 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 + required: true + schema: + $ref: '#/definitions/api.verifyEmployerEmailRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.verifyEmployerEmailResponse' + "400": + description: Invalid request body. + schema: + $ref: '#/definitions/api.ErrorResponse' + "500": + description: Any other error. + schema: + $ref: '#/definitions/api.ErrorResponse' + summary: Verify employer email + tags: + - employers /job-applications: post: consumes: diff --git a/internal/api/employer.go b/internal/api/employer.go index a86cd97..fcb1cad 100644 --- a/internal/api/employer.go +++ b/internal/api/employer.go @@ -572,3 +572,44 @@ func (server *Server) getEmployerAndCompanyDetails(ctx *gin.Context) { ctx.JSON(http.StatusOK, details) } + +type verifyEmployerEmailRequest struct { + ID int64 `form:"id" binding:"required,min=1"` + SecretCode string `form:"code" binding:"required,min=32"` +} + +type verifyEmployerEmailResponse struct { + Message string `json:"message"` +} + +// @Schemes +// @Summary Verify employer email +// @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." +// @Success 200 {object} verifyEmployerEmailResponse +// @Failure 400 {object} ErrorResponse "Invalid request body." +// @Failure 500 {object} ErrorResponse "Any other error." +// @Router /employers/verify-email [get] +// verifyEmployerEmail handles employer email verification +func (server *Server) verifyEmployerEmail(ctx *gin.Context) { + var request verifyEmployerEmailRequest + if err := ctx.ShouldBindQuery(&request); err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(err)) + return + } + + txResult, err := server.store.VerifyEmployerEmailTx(ctx, db.VerifyEmailTxParams{ + ID: request.ID, + SecretCode: request.SecretCode, + }) + if err != nil { + ctx.JSON(http.StatusInternalServerError, errorResponse(err)) + return + } + + if txResult.Employer.IsEmailVerified { + ctx.JSON(http.StatusOK, verifyEmployerEmailResponse{Message: "Successfully verified email"}) + } +} diff --git a/internal/api/employer_test.go b/internal/api/employer_test.go index a9a0d32..13076f8 100644 --- a/internal/api/employer_test.go +++ b/internal/api/employer_test.go @@ -1459,6 +1459,130 @@ func TestGetEmployerAndCompanyDetailsAPI(t *testing.T) { } } +func TestVerifyEmployerEmailAPI(t *testing.T) { + employer, _, _ := generateRandomEmployerAndCompany(t) + verifyEmail := db.VerifyEmail{ + ID: int64(utils.RandomInt(1, 1000)), + Email: employer.Email, + SecretCode: utils.RandomString(32), + IsUsed: false, + CreatedAt: time.Now(), + ExpiredAt: time.Now().Add(15 * time.Minute), + } + + type Query struct { + ID int64 `json:"id"` + Code string `json:"code"` + } + + testCases := []struct { + name string + query Query + buildStubs func(store *mockdb.MockStore) + checkResponse func(recorder *httptest.ResponseRecorder) + }{ + { + name: "OK", + query: Query{ + ID: verifyEmail.ID, + Code: verifyEmail.SecretCode, + }, + buildStubs: func(store *mockdb.MockStore) { + params := db.VerifyEmailTxParams{ + ID: verifyEmail.ID, + SecretCode: verifyEmail.SecretCode, + } + verifyEmail.IsUsed = true + employer.IsEmailVerified = true + store.EXPECT(). + VerifyEmployerEmailTx(gomock.Any(), gomock.Eq(params)). + Times(1). + Return(db.VerifyEmployerEmailResult{ + Employer: employer, + VerifyEmail: verifyEmail, + }, nil) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusOK, recorder.Code) + }, + }, + { + name: "Internal Server Error", + query: Query{ + ID: verifyEmail.ID, + Code: verifyEmail.SecretCode, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + VerifyEmployerEmailTx(gomock.Any(), gomock.Any()). + Times(1). + Return(db.VerifyEmployerEmailResult{}, sql.ErrConnDone) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusInternalServerError, recorder.Code) + }, + }, + { + name: "Invalid Code Length", + query: Query{ + ID: verifyEmail.ID, + Code: utils.RandomString(31), + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + VerifyEmployerEmailTx(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + name: "Invalid ID", + query: Query{ + ID: 0, + Code: verifyEmail.SecretCode, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + VerifyEmployerEmailTx(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + } + for i := range testCases { + tc := testCases[i] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mockdb.NewMockStore(ctrl) + tc.buildStubs(store) + + server := newTestServer(t, store, nil, nil) + recorder := httptest.NewRecorder() + + url := BaseUrl + "/employers/verify-email" + + req, err := http.NewRequest(http.MethodGet, url, nil) + require.NoError(t, err) + + q := req.URL.Query() + q.Add("id", fmt.Sprintf("%d", tc.query.ID)) + q.Add("code", tc.query.Code) + req.URL.RawQuery = q.Encode() + + server.router.ServeHTTP(recorder, req) + + tc.checkResponse(recorder) + }) + } +} + // generateRandomEmployer create a random employer and company func generateRandomEmployerAndCompany(t *testing.T) (db.Employer, string, db.Company) { password := utils.RandomString(6) diff --git a/internal/api/server.go b/internal/api/server.go index ea3c08c..624244d 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -84,6 +84,7 @@ func (server *Server) setupRouter() { // === employers === routerV1.POST("/employers", server.createEmployer) routerV1.POST("/employers/login", server.loginEmployer) + routerV1.GET("/employers/verify-email", server.verifyEmployerEmail) routerV1.GET("/employers/employer-company-details/:email", server.getEmployerAndCompanyDetails) diff --git a/internal/api/user.go b/internal/api/user.go index a75d5b4..27a442a 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -581,7 +581,7 @@ func (server *Server) verifyUserEmail(ctx *gin.Context) { return } - txResult, err := server.store.VerifyEmailTx(ctx, db.VerifyEmailTxParams{ + txResult, err := server.store.VerifyUserEmailTx(ctx, db.VerifyEmailTxParams{ ID: request.ID, SecretCode: request.SecretCode, }) diff --git a/internal/api/user_test.go b/internal/api/user_test.go index 5681fb1..4c4b8b9 100644 --- a/internal/api/user_test.go +++ b/internal/api/user_test.go @@ -1348,9 +1348,9 @@ func TestVerifyUserEmailAPI(t *testing.T) { verifyEmail.IsUsed = true user.IsEmailVerified = true store.EXPECT(). - VerifyEmailTx(gomock.Any(), gomock.Eq(params)). + VerifyUserEmailTx(gomock.Any(), gomock.Eq(params)). Times(1). - Return(db.VerifyEmailTxResult{ + Return(db.VerifyUserEmailResult{ User: user, VerifyEmail: verifyEmail, }, nil) @@ -1367,9 +1367,9 @@ func TestVerifyUserEmailAPI(t *testing.T) { }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). - VerifyEmailTx(gomock.Any(), gomock.Any()). + VerifyUserEmailTx(gomock.Any(), gomock.Any()). Times(1). - Return(db.VerifyEmailTxResult{}, sql.ErrConnDone) + Return(db.VerifyUserEmailResult{}, sql.ErrConnDone) }, checkResponse: func(recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusInternalServerError, recorder.Code) @@ -1383,7 +1383,7 @@ func TestVerifyUserEmailAPI(t *testing.T) { }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). - VerifyEmailTx(gomock.Any(), gomock.Any()). + VerifyUserEmailTx(gomock.Any(), gomock.Any()). Times(0) }, checkResponse: func(recorder *httptest.ResponseRecorder) { @@ -1398,7 +1398,7 @@ func TestVerifyUserEmailAPI(t *testing.T) { }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). - VerifyEmailTx(gomock.Any(), gomock.Any()). + VerifyUserEmailTx(gomock.Any(), gomock.Any()). Times(0) }, checkResponse: func(recorder *httptest.ResponseRecorder) { diff --git a/internal/db/migrations/000007_add_verify_emails.up.sql b/internal/db/migrations/000007_add_verify_emails.up.sql index 364208b..a14e723 100644 --- a/internal/db/migrations/000007_add_verify_emails.up.sql +++ b/internal/db/migrations/000007_add_verify_emails.up.sql @@ -8,6 +8,6 @@ CREATE TABLE "verify_emails" "expired_at" timestamptz NOT NULL DEFAULT (now() + interval '15 minutes') ); -ALTER TABLE "verify_emails" ADD FOREIGN KEY ("email") REFERENCES "users" ("email"); +-- ALTER TABLE "verify_emails" ADD FOREIGN KEY ("email") REFERENCES "users" ("email"); ALTER TABLE "users" ADD COLUMN "is_email_verified" bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/internal/db/migrations/000008_add_employer_verify_email.down.sql b/internal/db/migrations/000008_add_employer_verify_email.down.sql new file mode 100644 index 0000000..73db64e --- /dev/null +++ b/internal/db/migrations/000008_add_employer_verify_email.down.sql @@ -0,0 +1 @@ +ALTER TABLE "employers" DROP COLUMN "is_email_verified"; \ No newline at end of file diff --git a/internal/db/migrations/000008_add_employer_verify_email.up.sql b/internal/db/migrations/000008_add_employer_verify_email.up.sql new file mode 100644 index 0000000..c4b11c9 --- /dev/null +++ b/internal/db/migrations/000008_add_employer_verify_email.up.sql @@ -0,0 +1 @@ +ALTER TABLE "employers" ADD COLUMN "is_email_verified" bool NOT NULL DEFAULT false; diff --git a/internal/db/mock/store.go b/internal/db/mock/store.go index 5d24a9f..35d3b16 100644 --- a/internal/db/mock/store.go +++ b/internal/db/mock/store.go @@ -1084,19 +1084,34 @@ func (mr *MockStoreMockRecorder) UpdateVerifyEmail(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVerifyEmail", reflect.TypeOf((*MockStore)(nil).UpdateVerifyEmail), arg0, arg1) } -// VerifyEmailTx mocks base method. -func (m *MockStore) VerifyEmailTx(arg0 context.Context, arg1 db.VerifyEmailTxParams) (db.VerifyEmailTxResult, error) { +// VerifyEmployerEmail mocks base method. +func (m *MockStore) VerifyEmployerEmail(arg0 context.Context, arg1 string) (db.Employer, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VerifyEmailTx", arg0, arg1) - ret0, _ := ret[0].(db.VerifyEmailTxResult) + ret := m.ctrl.Call(m, "VerifyEmployerEmail", arg0, arg1) + ret0, _ := ret[0].(db.Employer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyEmployerEmail indicates an expected call of VerifyEmployerEmail. +func (mr *MockStoreMockRecorder) VerifyEmployerEmail(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyEmployerEmail", reflect.TypeOf((*MockStore)(nil).VerifyEmployerEmail), arg0, arg1) +} + +// VerifyEmployerEmailTx mocks base method. +func (m *MockStore) VerifyEmployerEmailTx(arg0 context.Context, arg1 db.VerifyEmailTxParams) (db.VerifyEmployerEmailResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifyEmployerEmailTx", arg0, arg1) + ret0, _ := ret[0].(db.VerifyEmployerEmailResult) ret1, _ := ret[1].(error) return ret0, ret1 } -// VerifyEmailTx indicates an expected call of VerifyEmailTx. -func (mr *MockStoreMockRecorder) VerifyEmailTx(arg0, arg1 interface{}) *gomock.Call { +// VerifyEmployerEmailTx indicates an expected call of VerifyEmployerEmailTx. +func (mr *MockStoreMockRecorder) VerifyEmployerEmailTx(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyEmailTx", reflect.TypeOf((*MockStore)(nil).VerifyEmailTx), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyEmployerEmailTx", reflect.TypeOf((*MockStore)(nil).VerifyEmployerEmailTx), arg0, arg1) } // VerifyUserEmail mocks base method. @@ -1113,3 +1128,18 @@ func (mr *MockStoreMockRecorder) VerifyUserEmail(arg0, arg1 interface{}) *gomock mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyUserEmail", reflect.TypeOf((*MockStore)(nil).VerifyUserEmail), arg0, arg1) } + +// VerifyUserEmailTx mocks base method. +func (m *MockStore) VerifyUserEmailTx(arg0 context.Context, arg1 db.VerifyEmailTxParams) (db.VerifyUserEmailResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VerifyUserEmailTx", arg0, arg1) + ret0, _ := ret[0].(db.VerifyUserEmailResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VerifyUserEmailTx indicates an expected call of VerifyUserEmailTx. +func (mr *MockStoreMockRecorder) VerifyUserEmailTx(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyUserEmailTx", reflect.TypeOf((*MockStore)(nil).VerifyUserEmailTx), arg0, arg1) +} diff --git a/internal/db/queries/employer.sql b/internal/db/queries/employer.sql index e6c097c..1e7ced7 100644 --- a/internal/db/queries/employer.sql +++ b/internal/db/queries/employer.sql @@ -26,6 +26,12 @@ UPDATE employers SET hashed_password = $2 WHERE id = $1; +-- name: VerifyEmployerEmail :one +UPDATE employers +SET is_email_verified = TRUE +WHERE email = $1 +RETURNING *; + -- name: DeleteEmployer :exec DELETE FROM employers diff --git a/internal/db/sqlc/company.sql.go b/internal/db/sqlc/company.sql.go index 0b2f41a..510396d 100644 --- a/internal/db/sqlc/company.sql.go +++ b/internal/db/sqlc/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: company.sql package db diff --git a/internal/db/sqlc/db.go b/internal/db/sqlc/db.go index e0b5347..bf8f8e3 100644 --- a/internal/db/sqlc/db.go +++ b/internal/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 package db diff --git a/internal/db/sqlc/employer.sql.go b/internal/db/sqlc/employer.sql.go index e46955a..dddb3dc 100644 --- a/internal/db/sqlc/employer.sql.go +++ b/internal/db/sqlc/employer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: employer.sql package db @@ -12,7 +12,7 @@ import ( const createEmployer = `-- name: CreateEmployer :one INSERT INTO employers (company_id, full_name, email, hashed_password) VALUES ($1, $2, $3, $4) -RETURNING id, company_id, full_name, email, hashed_password, created_at +RETURNING id, company_id, full_name, email, hashed_password, created_at, is_email_verified ` type CreateEmployerParams struct { @@ -37,6 +37,7 @@ func (q *Queries) CreateEmployer(ctx context.Context, arg CreateEmployerParams) &i.Email, &i.HashedPassword, &i.CreatedAt, + &i.IsEmailVerified, ) return i, err } @@ -91,7 +92,7 @@ func (q *Queries) GetEmployerAndCompanyDetails(ctx context.Context, email string } const getEmployerByEmail = `-- name: GetEmployerByEmail :one -SELECT id, company_id, full_name, email, hashed_password, created_at +SELECT id, company_id, full_name, email, hashed_password, created_at, is_email_verified FROM employers WHERE email = $1 ` @@ -106,12 +107,13 @@ func (q *Queries) GetEmployerByEmail(ctx context.Context, email string) (Employe &i.Email, &i.HashedPassword, &i.CreatedAt, + &i.IsEmailVerified, ) return i, err } const getEmployerByID = `-- name: GetEmployerByID :one -SELECT id, company_id, full_name, email, hashed_password, created_at +SELECT id, company_id, full_name, email, hashed_password, created_at, is_email_verified FROM employers WHERE id = $1 ` @@ -126,6 +128,7 @@ func (q *Queries) GetEmployerByID(ctx context.Context, id int32) (Employer, erro &i.Email, &i.HashedPassword, &i.CreatedAt, + &i.IsEmailVerified, ) return i, err } @@ -136,7 +139,7 @@ SET company_id = $2, full_name = $3, email = $4 WHERE id = $1 -RETURNING id, company_id, full_name, email, hashed_password, created_at +RETURNING id, company_id, full_name, email, hashed_password, created_at, is_email_verified ` type UpdateEmployerParams struct { @@ -161,6 +164,7 @@ func (q *Queries) UpdateEmployer(ctx context.Context, arg UpdateEmployerParams) &i.Email, &i.HashedPassword, &i.CreatedAt, + &i.IsEmailVerified, ) return i, err } @@ -180,3 +184,25 @@ func (q *Queries) UpdateEmployerPassword(ctx context.Context, arg UpdateEmployer _, err := q.db.ExecContext(ctx, updateEmployerPassword, arg.ID, arg.HashedPassword) return err } + +const verifyEmployerEmail = `-- name: VerifyEmployerEmail :one +UPDATE employers +SET is_email_verified = TRUE +WHERE email = $1 +RETURNING id, company_id, full_name, email, hashed_password, created_at, is_email_verified +` + +func (q *Queries) VerifyEmployerEmail(ctx context.Context, email string) (Employer, error) { + row := q.db.QueryRowContext(ctx, verifyEmployerEmail, email) + var i Employer + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.FullName, + &i.Email, + &i.HashedPassword, + &i.CreatedAt, + &i.IsEmailVerified, + ) + return i, err +} diff --git a/internal/db/sqlc/job.sql.go b/internal/db/sqlc/job.sql.go index 97da218..c14a193 100644 --- a/internal/db/sqlc/job.sql.go +++ b/internal/db/sqlc/job.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: job.sql package db diff --git a/internal/db/sqlc/job_application.sql.go b/internal/db/sqlc/job_application.sql.go index 5008e81..19ef67e 100644 --- a/internal/db/sqlc/job_application.sql.go +++ b/internal/db/sqlc/job_application.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: job_application.sql package db diff --git a/internal/db/sqlc/job_skill.sql.go b/internal/db/sqlc/job_skill.sql.go index f364810..d07969a 100644 --- a/internal/db/sqlc/job_skill.sql.go +++ b/internal/db/sqlc/job_skill.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: job_skill.sql package db diff --git a/internal/db/sqlc/models.go b/internal/db/sqlc/models.go index efa221a..4b0bba8 100644 --- a/internal/db/sqlc/models.go +++ b/internal/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 package db @@ -34,8 +34,8 @@ func (e *ApplicationStatus) Scan(src interface{}) error { } type NullApplicationStatus struct { - ApplicationStatus ApplicationStatus - Valid bool // Valid is true if ApplicationStatus is not NULL + ApplicationStatus ApplicationStatus `json:"application_status"` + Valid bool `json:"valid"` // Valid is true if ApplicationStatus is not NULL } // Scan implements the Scanner interface. @@ -64,12 +64,13 @@ type Company struct { } type Employer struct { - ID int32 `json:"id"` - CompanyID int32 `json:"company_id"` - FullName string `json:"full_name"` - Email string `json:"email"` - HashedPassword string `json:"hashed_password"` - CreatedAt time.Time `json:"created_at"` + ID int32 `json:"id"` + CompanyID int32 `json:"company_id"` + FullName string `json:"full_name"` + Email string `json:"email"` + HashedPassword string `json:"hashed_password"` + CreatedAt time.Time `json:"created_at"` + IsEmailVerified bool `json:"is_email_verified"` } type Job struct { diff --git a/internal/db/sqlc/querier.go b/internal/db/sqlc/querier.go index 504dd10..1c7b7d4 100644 --- a/internal/db/sqlc/querier.go +++ b/internal/db/sqlc/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 package db @@ -74,6 +74,7 @@ type Querier interface { UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) UpdateUserSkill(ctx context.Context, arg UpdateUserSkillParams) (UserSkill, error) UpdateVerifyEmail(ctx context.Context, arg UpdateVerifyEmailParams) (VerifyEmail, error) + VerifyEmployerEmail(ctx context.Context, email string) (Employer, error) VerifyUserEmail(ctx context.Context, email string) (User, error) } diff --git a/internal/db/sqlc/store.go b/internal/db/sqlc/store.go index 818ab0e..9522593 100644 --- a/internal/db/sqlc/store.go +++ b/internal/db/sqlc/store.go @@ -17,7 +17,8 @@ type Store interface { CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) CreateEmployerTx(ctx context.Context, arg CreateEmployerTxParams) (CreateEmployerTxResult, error) ExecTx(ctx context.Context, fn func(*Queries) error) error - VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error) + VerifyUserEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyUserEmailResult, error) + VerifyEmployerEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmployerEmailResult, error) } // SQLStore provides all functions to execute db queries and transactions diff --git a/internal/db/sqlc/tx_verify_email.go b/internal/db/sqlc/tx_verify_email.go index ddddb0c..9b60d37 100644 --- a/internal/db/sqlc/tx_verify_email.go +++ b/internal/db/sqlc/tx_verify_email.go @@ -9,14 +9,14 @@ type VerifyEmailTxParams struct { SecretCode string } -type VerifyEmailTxResult struct { +type VerifyUserEmailResult struct { User User VerifyEmail VerifyEmail } -// VerifyEmailTx verify email transaction -func (store *SQLStore) VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error) { - var result VerifyEmailTxResult +// VerifyUserEmailTx verify user email transaction +func (store *SQLStore) VerifyUserEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyUserEmailResult, error) { + var result VerifyUserEmailResult err := store.ExecTx(ctx, func(q *Queries) error { var err error @@ -35,3 +35,30 @@ func (store *SQLStore) VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParam return result, err } + +type VerifyEmployerEmailResult struct { + Employer Employer + VerifyEmail VerifyEmail +} + +// VerifyEmployerEmailTx verify employer email transaction +func (store *SQLStore) VerifyEmployerEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmployerEmailResult, error) { + var result VerifyEmployerEmailResult + + err := store.ExecTx(ctx, func(q *Queries) error { + var err error + + result.VerifyEmail, err = q.UpdateVerifyEmail(ctx, UpdateVerifyEmailParams{ + ID: arg.ID, + SecretCode: arg.SecretCode, + }) + if err != nil { + return err + } + + result.Employer, err = q.VerifyEmployerEmail(ctx, result.VerifyEmail.Email) + return err + }) + + return result, err +} diff --git a/internal/db/sqlc/tx_verify_email_test.go b/internal/db/sqlc/tx_verify_email_test.go index 115fd87..7af3c9d 100644 --- a/internal/db/sqlc/tx_verify_email_test.go +++ b/internal/db/sqlc/tx_verify_email_test.go @@ -6,15 +6,15 @@ import ( "testing" ) -func TestSQLStore_VerifyEmailTx(t *testing.T) { - verifyEmail := createRandomVerifyEmail(t) +func TestSQLStore_VerifyUserEmailTx(t *testing.T) { + verifyEmail := createVerifyEmailForUser(t) params := VerifyEmailTxParams{ ID: verifyEmail.ID, SecretCode: verifyEmail.SecretCode, } store := NewStore(testDB) - result, err := store.VerifyEmailTx(context.Background(), params) + result, err := store.VerifyUserEmailTx(context.Background(), params) require.NoError(t, err) require.Equal(t, verifyEmail.ID, result.VerifyEmail.ID) require.Equal(t, verifyEmail.Email, result.VerifyEmail.Email) @@ -24,3 +24,22 @@ func TestSQLStore_VerifyEmailTx(t *testing.T) { require.NotEmpty(t, result.User.Email) require.Equal(t, verifyEmail.Email, result.User.Email) } + +func TestSQLStore_VerifyEmployerEmailTx(t *testing.T) { + verifyEmail := createVerifyEmailForEmployer(t) + params := VerifyEmailTxParams{ + ID: verifyEmail.ID, + SecretCode: verifyEmail.SecretCode, + } + + store := NewStore(testDB) + result, err := store.VerifyEmployerEmailTx(context.Background(), params) + require.NoError(t, err) + require.Equal(t, verifyEmail.ID, result.VerifyEmail.ID) + require.Equal(t, verifyEmail.Email, result.VerifyEmail.Email) + require.Equal(t, verifyEmail.SecretCode, result.VerifyEmail.SecretCode) + require.NotEmpty(t, result.Employer) + require.NotEmpty(t, result.Employer.ID) + require.NotEmpty(t, result.Employer.Email) + require.Equal(t, verifyEmail.Email, result.Employer.Email) +} diff --git a/internal/db/sqlc/user.sql.go b/internal/db/sqlc/user.sql.go index ff67c80..af50668 100644 --- a/internal/db/sqlc/user.sql.go +++ b/internal/db/sqlc/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: user.sql package db diff --git a/internal/db/sqlc/user_skill.sql.go b/internal/db/sqlc/user_skill.sql.go index 0e828cd..841f3e4 100644 --- a/internal/db/sqlc/user_skill.sql.go +++ b/internal/db/sqlc/user_skill.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: user_skill.sql package db diff --git a/internal/db/sqlc/verify_email.sql.go b/internal/db/sqlc/verify_email.sql.go index cfb7889..526087e 100644 --- a/internal/db/sqlc/verify_email.sql.go +++ b/internal/db/sqlc/verify_email.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.20.0 // source: verify_email.sql package db diff --git a/internal/db/sqlc/verify_email_test.go b/internal/db/sqlc/verify_email_test.go index 4b371ea..737bf06 100644 --- a/internal/db/sqlc/verify_email_test.go +++ b/internal/db/sqlc/verify_email_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func createRandomVerifyEmail(t *testing.T) VerifyEmail { +func createVerifyEmailForUser(t *testing.T) VerifyEmail { user := createRandomUser(t) params := CreateVerifyEmailParams{ Email: user.Email, @@ -26,12 +26,32 @@ func createRandomVerifyEmail(t *testing.T) VerifyEmail { return verifyEmail } +func createVerifyEmailForEmployer(t *testing.T) VerifyEmail { + employer := createRandomEmployer(t, 0) + params := CreateVerifyEmailParams{ + Email: employer.Email, + SecretCode: utils.RandomString(32), + } + + verifyEmail, err := testQueries.CreateVerifyEmail(context.Background(), params) + require.NoError(t, err) + require.Equal(t, verifyEmail.Email, employer.Email) + require.Equal(t, verifyEmail.SecretCode, params.SecretCode) + require.NotZero(t, verifyEmail.ID) + require.NotZero(t, verifyEmail.CreatedAt) + require.NotZero(t, verifyEmail.ExpiredAt) + require.True(t, verifyEmail.ExpiredAt.After(verifyEmail.CreatedAt)) + + return verifyEmail +} + func TestQueries_CreateVerifyEmail(t *testing.T) { - createRandomUser(t) + createVerifyEmailForUser(t) + createVerifyEmailForEmployer(t) } func TestQueries_UpdateVerifyEmail(t *testing.T) { - verifyEmail := createRandomVerifyEmail(t) + verifyEmail := createVerifyEmailForUser(t) params := UpdateVerifyEmailParams{ ID: verifyEmail.ID, SecretCode: verifyEmail.SecretCode, diff --git a/internal/worker/task_send_verification_email.go b/internal/worker/task_send_verification_email.go index 825bcb9..80593db 100644 --- a/internal/worker/task_send_verification_email.go +++ b/internal/worker/task_send_verification_email.go @@ -2,6 +2,7 @@ package worker import ( "context" + "database/sql" "encoding/json" "fmt" db "github.com/aalug/go-gin-job-search/internal/db/sqlc" @@ -40,6 +41,7 @@ func (distributor *RedisTaskDistributor) DistributeTaskSendVerificationEmail( } // ProcessTaskSendVerificationEmail processes the task of sending a verification email. +// It works for both employers and users. func (processor *RedisTaskProcessor) ProcessTaskSendVerificationEmail(ctx context.Context, task *asynq.Task) error { var payload PayloadSendVerificationEmail err := json.Unmarshal(task.Payload(), &payload) @@ -47,14 +49,30 @@ func (processor *RedisTaskProcessor) ProcessTaskSendVerificationEmail(ctx contex return fmt.Errorf("failed to unmarshal payload: %w", asynq.SkipRetry) } + var email string + var fullName string user, err := processor.store.GetUserByEmail(ctx, payload.Email) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + if err == sql.ErrNoRows { + // it might be an employer + employer, err := processor.store.GetEmployerByEmail(ctx, payload.Email) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + email = employer.Email + fullName = employer.FullName + } else { + return fmt.Errorf("failed to get user: %w", err) + } + + } else { + email = user.Email + fullName = user.FullName } // create verify email in the database verifyEmail, err := processor.store.CreateVerifyEmail(ctx, db.CreateVerifyEmailParams{ - Email: user.Email, + Email: email, SecretCode: utils.RandomString(32), }) if err != nil { @@ -62,7 +80,7 @@ func (processor *RedisTaskProcessor) ProcessTaskSendVerificationEmail(ctx contex } // send email to user to verify email - verifyUrl := fmt.Sprintf("/%s%s/users/verify-email?id=%d&code=%s", + verifyUrl := fmt.Sprintf("/%s%s/employers/verify-email?id=%d&code=%s", processor.config.ServerAddress, processor.config.BaseUrl, verifyEmail.ID, verifyEmail.SecretCode) content := fmt.Sprintf(`

Hello %s


@@ -70,9 +88,9 @@ func (processor *RedisTaskProcessor) ProcessTaskSendVerificationEmail(ctx contex Please click the link below to verify your email address:

Verify Email - `, user.FullName, verifyUrl) + `, fullName, verifyUrl) err = processor.emailSender.SendEmail(mail.Data{ - To: []string{user.Email}, + To: []string{email}, Subject: "Welcome to Go Job Search!", Content: content, Template: "verification_email.html",