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",