Skip to content

Commit

Permalink
impl email verification
Browse files Browse the repository at this point in the history
  • Loading branch information
aradwann committed Jan 15, 2024
1 parent 9cbd9a7 commit 11bb7e9
Show file tree
Hide file tree
Showing 21 changed files with 696 additions and 111 deletions.
18 changes: 18 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"plpgsqlLanguageServer.database": "eenergy",
"plpgsqlLanguageServer.user": "root",
"plpgsqlLanguageServer.password": "secret",
"plpgsqlLanguageServer.definitionFiles": [
// Support glob.
"**/*.sql",
"**/*.psql",
"**/*.pgsql"
],
// The supported extention types are ['*.pgsql', '*.psql'].
// If you want to use this extension in '*.sql', add the following settings.
"files.associations": {
"*.sql": "postgres"
}
}


6 changes: 4 additions & 2 deletions db/migrations/procs/user/update_user.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ CREATE OR REPLACE FUNCTION update_user(
p_hashed_password VARCHAR,
p_password_changed_at TIMESTAMP WITH TIME ZONE,
p_fullname VARCHAR,
p_email VARCHAR
p_email VARCHAR,
p_is_email_verified BOOLEAN
)
RETURNS TABLE (
username VARCHAR,
Expand All @@ -22,7 +23,8 @@ BEGIN
hashed_password = COALESCE(p_hashed_password, users.hashed_password),
password_changed_at = COALESCE(p_password_changed_at, users.password_changed_at),
fullname = COALESCE(p_fullname, users.fullname),
email = COALESCE(p_email, users.email)
email = COALESCE(p_email, users.email),
is_email_verified = COALESCE(p_is_email_verified, users.is_email_verified)
WHERE users.username = p_username
RETURNING *;
END;
Expand Down
34 changes: 34 additions & 0 deletions db/migrations/procs/user/update_verify_email.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
DROP FUNCTION IF EXISTS update_verify_email;
CREATE OR REPLACE FUNCTION update_verify_email(
p_id bigint,
p_secret_code VARCHAR
)
RETURNS TABLE (
id bigint,
username VARCHAR,
email VARCHAR,
secret_code VARCHAR,
is_used BOOLEAN,
created_at TIMESTAMP WITH TIME ZONE,
expires_at_at TIMESTAMP WITH TIME ZONE
) AS $$
BEGIN
RETURN QUERY
UPDATE verify_emails
SET is_used = TRUE
WHERE
verify_emails.id = p_id
AND verify_emails.secret_code = p_secret_code
AND verify_emails.is_used = FALSE
AND verify_emails.expired_at > now()
RETURNING
verify_emails.id,
verify_emails.username,
verify_emails.email,
verify_emails.secret_code,
verify_emails.is_used,
verify_emails.created_at,
verify_emails.expired_at;

END;
$$ LANGUAGE plpgsql;
15 changes: 15 additions & 0 deletions db/mock/store.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion db/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
type Store interface {
Querier
CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error)
// VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error)
VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error)
}

// SQLStore provides all functions to execute db queries and transactions
Expand Down
43 changes: 43 additions & 0 deletions db/store/tx_verify_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package db

import (
"context"
"database/sql"
)

type VerifyEmailTxParams struct {
EmailId int64
SecretCode string
}

type VerifyEmailTxResult struct {
User User
VerifyEmail VerifyEmail
}

func (store *SQLStore) VerifyEmailTx(ctx context.Context, arg VerifyEmailTxParams) (VerifyEmailTxResult, error) {
var result VerifyEmailTxResult

err := store.execTx(ctx, func(q *Queries) error {
var err error
result.VerifyEmail, err = q.UpdatVerifyEmail(ctx, UpdatVerifyEmailParams{
ID: arg.EmailId,
SecretCode: arg.SecretCode,
})
if err != nil {
return err
}
result.User, err = q.UpdateUser(ctx, UpdateUserParams{
Username: result.VerifyEmail.Username,
IsEmailVerified: sql.NullBool{
Bool: true,
Valid: true,
},
})

return err

})

return result, err
}
2 changes: 2 additions & 0 deletions db/store/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type UpdateUserParams struct {
FullName sql.NullString `json:"fullname"`
Email sql.NullString `json:"email"`
Username string `json:"username"`
IsEmailVerified sql.NullBool `json:"is_email_verified"`
}

func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
Expand All @@ -54,6 +55,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e
arg.PasswordChangedAt,
arg.FullName,
arg.Email,
arg.IsEmailVerified,
}
row := q.callStoredFunction(ctx, "update_user", params...)

Expand Down
42 changes: 18 additions & 24 deletions db/store/verify_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,27 @@ func (q *Queries) CreateVerifyEmail(ctx context.Context, arg CreateVerifyEmail)
// return user, nil
// }

// type UpdateUserParams struct {
// HashedPassword sql.NullString `json:"hashed_password"`
// PasswordChangedAt sql.NullTime `json:"password_changed_at"`
// FullName sql.NullString `json:"fullname"`
// Email sql.NullString `json:"email"`
// Username string `json:"username"`
// }
type UpdatVerifyEmailParams struct {
ID int64 `json:"ID"`
SecretCode string `json:"secret_code"`
}

// func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
// var user User
// params := []interface{}{
// arg.Username,
// arg.HashedPassword,
// arg.PasswordChangedAt,
// arg.FullName,
// arg.Email,
// }
// row := q.callStoredFunction(ctx, "update_user", params...)
func (q *Queries) UpdatVerifyEmail(ctx context.Context, arg UpdatVerifyEmailParams) (VerifyEmail, error) {
var verifyEmail VerifyEmail
params := []interface{}{
arg.ID,
arg.SecretCode,
}
row := q.callStoredFunction(ctx, "update_verify_email", params...)

// // Execute the stored procedure and scan the results into the variables
// err := scanUserFromRow(row, &user)
// if err != nil {
// return User{}, err
// }
// Execute the stored procedure and scan the results into the variables
err := scanVerifyEmailFromRow(row, &verifyEmail)
if err != nil {
return verifyEmail, err
}

// return user, nil
// }
return verifyEmail, nil
}

func scanVerifyEmailFromRow(row *sql.Row, verifyEmail *VerifyEmail) error {
err := row.Scan(
Expand Down
47 changes: 47 additions & 0 deletions doc/swagger/eenergy.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,45 @@
"EenergyService"
]
}
},
"/v1/verify_email": {
"get": {
"summary": "verify email",
"description": "Use this API to verify the user email",
"operationId": "EenergyService_VerifyEmail",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/pbVerifyEmailResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "emailId",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "secretCode",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"EenergyService"
]
}
}
},
"definitions": {
Expand Down Expand Up @@ -233,6 +272,14 @@
}
}
},
"pbVerifyEmailResponse": {
"type": "object",
"properties": {
"isVerified": {
"type": "boolean"
}
}
},
"protobufAny": {
"type": "object",
"properties": {
Expand Down
1 change: 0 additions & 1 deletion gapi/rpc_create_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest)
}
return nil, status.Errorf(codes.Internal, "failed to create user: %s", err)
}
// Send verification email

rsp := &pb.CreateUserResponse{
User: convertUser(result.User),
Expand Down
41 changes: 41 additions & 0 deletions gapi/rpc_verify_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gapi

import (
"context"

db "github.com/aradwann/eenergy/db/store"
"github.com/aradwann/eenergy/pb"
"github.com/aradwann/eenergy/val"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func (server *Server) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest) (*pb.VerifyEmailResponse, error) {
violations := validateVerifyEmailRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
txResult, err := server.store.VerifyEmailTx(ctx, db.VerifyEmailTxParams{
EmailId: req.GetEmailId(),
SecretCode: req.GetSecretCode(),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to verify email")
}
rsp := &pb.VerifyEmailResponse{
IsVerified: txResult.User.IsEmailVerified,
}
return rsp, nil
}

func validateVerifyEmailRequest(req *pb.VerifyEmailRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateEmailId(req.GetEmailId()); err != nil {
violations = append(violations, fieldViolation("email_id", err))
}
if err := val.ValidateSecretCode(req.GetSecretCode()); err != nil {
violations = append(violations, fieldViolation("secret_code", err))
}

return
}
18 changes: 9 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ require (
github.com/google/uuid v1.5.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0
github.com/hibiken/asynq v0.24.1
github.com/jackc/pgx/v5 v5.5.1
github.com/jackc/pgx/v5 v5.5.2
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/o1egl/paseto v1.0.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
go.uber.org/mock v0.4.0
golang.org/x/crypto v0.17.0
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917
golang.org/x/crypto v0.18.0
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1
google.golang.org/grpc v1.60.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.32.0
Expand All @@ -32,16 +32,16 @@ require (
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgtype v1.14.1 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/redis/go-redis/v9 v9.3.1 // indirect
github.com/redis/go-redis/v9 v9.4.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
)

require (
Expand All @@ -67,7 +67,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
Loading

0 comments on commit 11bb7e9

Please sign in to comment.