Dwi Wahyudi
Senior Software Engineer (Ruby, Golang, Java)
In previous part, we’ve completed our endpoint for user registration and sending the email verification/confirmation to account’s email afterward, inside that email, user can find and click a link of an endpoint to verify/confirm the email address. Now in this article, we’ll create such endpoint.
Update in Repo
Update in Account Email Confirmation Repo
First thing we need to do is to add a new query for this activity:
- update
account_email_confirmation
, based on some criteria:confirmation_hash
parameter.- non expired confirmation hash (
created_at
must be newer than 6 hours ago). is_active
is true.
- If found and successfully updated, return the
account_id
.
And here’s the query that we’ll add to repo/accountemailconfirmationrepo/pg_query.go
:
updateByConfirmationHash = `
UPDATE
account_email_confirmation
SET
is_active = FALSE
WHERE
confirmation_hash = $1
AND is_active IS TRUE
AND created_at > NOW() - INTERVAL '6 HOURS'
RETURNING
account_id;`
If no such data is found, than we’re going to return this new error type (in entity/error.go
).
ErrConfirmationHashNotFound = fmt.Errorf("invalid confirmation hash")
Now, it’s time to create the repo method in repo/accountrepo/pg_tx.go
for executing such statement, do note that this query returns account_id
, so here we are using QueryRowContext
and Scan
.
func (a *accountEmailConfirmationPg) DeactivateAndReturnAccountIDByConfirmationHashTx(
ctx context.Context, tx sql.Tx, confirmationHash string,
) (int64, error) {
var accountID int64
err := tx.QueryRowContext(ctx, updateByConfirmationHash, confirmationHash).Scan(&accountID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, entity.ErrConfirmationHashNotFound
}
return 0, err
}
return accountID, nil
}
And let’s not forget to expose this implementation to AccountEmailConfirmationRepo
interface in repo/accountemailconfirmationrepo/account_email_confirmation_repo.go
:
AccountEmailConfirmationRepo interface {
// .. skipped for brevity
DeactivateAndReturnAccountIDByConfirmationHashTx(
ctx context.Context, tx sql.Tx, confirmationHash string,
) (int64, error)
}
Update in Account Repo
After getting account_id
from above repo method, we can then update email_verified
field of account. Let’s add new query in repo/accountrepo/pg_query.go
:
verifyEmail = `UPDATE account SET email_verified = TRUE WHERE id = $1;`
Let’s execute such query with this new method in repo/accountrepo/pg_tx.go
:
func (a *accountPg) VerifyEmailTx(ctx context.Context, tx sql.Tx, accountID int64) error {
_, err := tx.ExecContext(ctx, verifyEmail, accountID)
if err != nil {
return err
}
return nil
}
Add it to repo/accountrepo/account_repo.go
:
AccountRepo interface {
// .. skipped for brevity
VerifyEmailTx(ctx context.Context, tx sql.Tx, accountID int64) error
}
Update in Registration Domain Repo
Let’s call those 2 new methods in repo/registrationdomainrepo/registration_domain_repo.go
:
// .. skipped for brevity
type (
RegistrationDomainRepo interface {
// .. skipped for brevity
AccountVerifyEmail(ctx context.Context, confirmationHash string) error
}
// .. skipped for brevity
func (r *registrationDomain) AccountVerifyEmail(
ctx context.Context, confirmationHash string,
) error {
err := repocommon.WithTransaction(ctx, r.db.Write.DB, func(tx sql.Tx) error {
accountID, err := r.accountEmailConfirmationRepo.
DeactivateAndReturnAccountIDByConfirmationHashTx(ctx, tx, confirmationHash)
if err != nil {
return err
}
err = r.accountRepo.VerifyEmailTx(ctx, tx, accountID)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
We update account’s email confirmation and get ira accountID
, and finally pass it to VerifyEmailTx
method.
Update in Service and Restful Layers
Update in Registration Domain Service
Our new code in service/registrationdomainservice/registration_domain_service.go
will be simple and small:
// .. skipped for brevity
type (
RegistrationDomainService interface {
// .. skipped for brevity
AccountVerifyEmail(ctx context.Context, confirmationHash string) error
}
// .. skipped for brevity
func (r *registrationDomainService) AccountVerifyEmail(
ctx context.Context, confirmationHash string,
) error {
err := r.registrationDomainRepo.AccountVerifyEmail(ctx, confirmationHash)
if err != nil {
return err
}
return nil
}
Update in Registration Domain Restful
Our new restful handler will be like this, in restful/v1/registrationrestful/registration_restful.go
:
// .. skipped for brevity
type (
RegistrationRestful interface {
// .. skipped for brevity
AccountVerifyEmail(w http.ResponseWriter, r *http.Request)
}
// .. skipped for brevity
func (reg *registrationRestful) AccountVerifyEmail(w http.ResponseWriter, r *http.Request) {
fmt.Println("tessst")
confirmationHash := r.URL.Query().Get("confirmation_hash")
if confirmationHash == "" {
httpcommon.SetResponse(w, http.StatusBadRequest, "bad request")
return
}
err := reg.registrationDomainService.AccountVerifyEmail(r.Context(), confirmationHash)
if err != nil {
if errors.Is(err, entity.ErrConfirmationHashNotFound) {
httpcommon.SetResponse(w, http.StatusUnprocessableEntity, "confirmation hash not found")
return
}
httpcommon.SetResponse(w, http.StatusInternalServerError, "unknown error")
return
}
httpcommon.SetResponse(w, http.StatusCreated, "account email verified")
}
Add this restful handler to our new restful route:
// .. skipped for brevity
r.Route("/v1", func(v1Route chi.Router) {
v1Route.Route("/accounts", func(accountRoute chi.Router) {
accountRoute.Route("/registration", func(registrationRoute chi.Router) {
registrationRoute.Post("/", registrationRestful.EmailConfirmation)
registrationRoute.Get("/email_confirmation", registrationRestful.AccountVerifyEmail) // new route
})
})
})
Testing Our API
When successfully verified, we will confirm that the new API endpoint returns 201
:
Which means, account’s email is already verified. When trying again, we should return 422
: