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: