diff --git a/Makefile b/Makefile index d20420b..9a6db62 100644 --- a/Makefile +++ b/Makefile @@ -51,4 +51,7 @@ proto: evans: evans --host localhost --port 9090 -r repl -.PHONY: postgres createdb dropdb migrateup migratedown migrateup1 migratedown1 db_docs db_schema sqlc test server mock proto evans \ No newline at end of file +redis: + docker run --name redis -p 6379:6379 -d redis:7.2.3-alpine + +.PHONY: postgres createdb dropdb migrateup migratedown migrateup1 migratedown1 db_docs db_schema sqlc test server mock proto evans redis \ No newline at end of file diff --git a/app.env b/app.env index e1ad94c..aeaad29 100644 --- a/app.env +++ b/app.env @@ -1,3 +1,4 @@ +ENVIRONMENT=development DB_DRIVER=postgres DB_SOURCE=postgresql://root:secret@localhost:5432/bankofasia?sslmode=disable MIGRATION_URL=file://db/migration @@ -5,4 +6,5 @@ HTTP_SERVER_ADDRESS=0.0.0.0:8080 GRPC_SERVER_ADDRESS=0.0.0.0:9090 TOKEN_SYMMETRIC_KEY=01234567890123456789012345678901 ACCESS_TOKEN_DURATION=15m -REFRESH_TOKEN_DURATION=24h \ No newline at end of file +REFRESH_TOKEN_DURATION=24h +REDIS_ADDRESS=0.0.0.0:6379 \ No newline at end of file diff --git a/db/mock/store.go b/db/mock/store.go index 0c4604d..b88ce4f 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -126,6 +126,21 @@ func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) } +// CreateUserTx mocks base method. +func (m *MockStore) CreateUserTx(arg0 context.Context, arg1 db.CreateUserTxParams) (db.CreateUserTxResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserTx", arg0, arg1) + ret0, _ := ret[0].(db.CreateUserTxResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUserTx indicates an expected call of CreateUserTx. +func (mr *MockStoreMockRecorder) CreateUserTx(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserTx", reflect.TypeOf((*MockStore)(nil).CreateUserTx), arg0, arg1) +} + // DeleteAccount mocks base method. func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() @@ -304,3 +319,18 @@ func (mr *MockStoreMockRecorder) UpdateAccount(arg0, arg1 interface{}) *gomock.C mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccount", reflect.TypeOf((*MockStore)(nil).UpdateAccount), arg0, arg1) } + +// UpdateUser mocks base method. +func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1) + ret0, _ := ret[0].(db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUser indicates an expected call of UpdateUser. +func (mr *MockStoreMockRecorder) UpdateUser(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0, arg1) +} diff --git a/db/query/user.sql b/db/query/user.sql index 2c58c82..0e7228a 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -13,3 +13,13 @@ RETURNING *; SELECT * FROM users WHERE username = $1 LIMIT 1; +-- name: UpdateUser :one +UPDATE users +SET + hashed_password = COALESCE(sqlc.narg(hashed_password), hashed_password), + password_changed_at = COALESCE(sqlc.narg(password_changed_at), password_changed_at), + full_name = COALESCE(sqlc.narg(full_name), full_name), + email = COALESCE(sqlc.narg(email), email) +WHERE + username = sqlc.arg(username) +RETURNING *; \ No newline at end of file diff --git a/db/sqlc/account.sql.go b/db/sqlc/account.sql.go index 925e201..63d1329 100644 --- a/db/sqlc/account.sql.go +++ b/db/sqlc/account.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: account.sql package db diff --git a/db/sqlc/db.go b/db/sqlc/db.go index e0b5347..3d2b5bf 100644 --- a/db/sqlc/db.go +++ b/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db diff --git a/db/sqlc/entry.sql.go b/db/sqlc/entry.sql.go index 803fdf9..636da45 100644 --- a/db/sqlc/entry.sql.go +++ b/db/sqlc/entry.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: entry.sql package db diff --git a/db/sqlc/models.go b/db/sqlc/models.go index 3650e26..aefd70f 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 570ae74..bb7db2f 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db @@ -28,6 +28,7 @@ type Querier interface { ListEntries(ctx context.Context, arg ListEntriesParams) ([]Entry, error) ListTransfers(ctx context.Context, arg ListTransfersParams) ([]Transfer, error) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) + UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) } var _ Querier = (*Queries)(nil) diff --git a/db/sqlc/session.sql.go b/db/sqlc/session.sql.go index c83ee61..f191e32 100644 --- a/db/sqlc/session.sql.go +++ b/db/sqlc/session.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: session.sql package db diff --git a/db/sqlc/store.go b/db/sqlc/store.go index 39ec6ae..31e8f50 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -10,6 +10,7 @@ import ( type Store interface { Querier TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) + CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) } // SQLStore provides all functions to execute SQL queries and transactions @@ -42,87 +43,3 @@ func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) erro return tx.Commit() } - -// TransferTxParams contains all input parameters of the transfer transaction -type TransferTxParams struct { - FromAccountID int64 `json:from_account_id` - ToAccountID int64 `json:to_account_id` - Amount int64 `json:amount` -} - -// TransferTxResult is the result of the transfer transaction -type TransferTxResult struct { - Transfer Transfer `json:transfer` - FromAccount Account `json:from_account` - ToAccount Account `json:to_account` - FromEntry Entry `json:from_entry` - ToEntry Entry `json:to_entry` -} - -// TransferTx performs a money transfer from one account to the other -// It creates a transfer record, add account entries, and update accounts' balance within a single DB transaction -func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { - var result TransferTxResult - - err := store.execTx(ctx, func(q *Queries) error { - var err error - - result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ - FromAccountID: arg.FromAccountID, - ToAccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } - - result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.FromAccountID, - Amount: -arg.Amount, - }) - if err != nil { - return err - } - - result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } - - if arg.FromAccountID < arg.ToAccountID { - result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) - } else { - result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) - } - - return nil - }) - - return result, err -} - -func addMoney( - ctx context.Context, - q *Queries, - accountID1 int64, - amount1 int64, - accountID2 int64, - amount2 int64, -) (account1 Account, account2 Account, err error) { - account1, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ - ID: accountID1, - Amount: amount1, - }) - if err != nil { - return - } - - account2, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ - ID: accountID2, - Amount: amount2, - }) - return -} diff --git a/db/sqlc/transfer.sql.go b/db/sqlc/transfer.sql.go index 3b1c95f..f75db4b 100644 --- a/db/sqlc/transfer.sql.go +++ b/db/sqlc/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: transfer.sql package db diff --git a/db/sqlc/tx_create_user.go b/db/sqlc/tx_create_user.go new file mode 100644 index 0000000..eea7500 --- /dev/null +++ b/db/sqlc/tx_create_user.go @@ -0,0 +1,29 @@ +package db + +import "context" + +type CreateUserTxParams struct { + CreateUserParams + AfterCreate func(user User) error +} + +type CreateUserTxResult struct { + User User +} + +func (store *SQLStore) CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) { + var result CreateUserTxResult + + err := store.execTx(ctx, func(q *Queries) error { + var err error + + result.User, err = q.CreateUser(ctx, arg.CreateUserParams) + if err != nil { + return err + } + + return arg.AfterCreate(result.User) + }) + + return result, err +} diff --git a/db/sqlc/tx_transfer.go b/db/sqlc/tx_transfer.go new file mode 100644 index 0000000..9ca6b74 --- /dev/null +++ b/db/sqlc/tx_transfer.go @@ -0,0 +1,87 @@ +package db + +import "context" + +// TransferTxParams contains all input parameters of the transfer transaction +type TransferTxParams struct { + FromAccountID int64 `json:from_account_id` + ToAccountID int64 `json:to_account_id` + Amount int64 `json:amount` +} + +// TransferTxResult is the result of the transfer transaction +type TransferTxResult struct { + Transfer Transfer `json:transfer` + FromAccount Account `json:from_account` + ToAccount Account `json:to_account` + FromEntry Entry `json:from_entry` + ToEntry Entry `json:to_entry` +} + +// TransferTx performs a money transfer from one account to the other +// It creates a transfer record, add account entries, and update accounts' balance within a single DB transaction +func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { + var result TransferTxResult + + err := store.execTx(ctx, func(q *Queries) error { + var err error + + result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ + FromAccountID: arg.FromAccountID, + ToAccountID: arg.ToAccountID, + Amount: arg.Amount, + }) + if err != nil { + return err + } + + result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.FromAccountID, + Amount: -arg.Amount, + }) + if err != nil { + return err + } + + result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.ToAccountID, + Amount: arg.Amount, + }) + if err != nil { + return err + } + + if arg.FromAccountID < arg.ToAccountID { + result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) + } else { + result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) + } + + return nil + }) + + return result, err +} + +func addMoney( + ctx context.Context, + q *Queries, + accountID1 int64, + amount1 int64, + accountID2 int64, + amount2 int64, +) (account1 Account, account2 Account, err error) { + account1, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ + ID: accountID1, + Amount: amount1, + }) + if err != nil { + return + } + + account2, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ + ID: accountID2, + Amount: amount2, + }) + return +} diff --git a/db/sqlc/user.sql.go b/db/sqlc/user.sql.go index 113a81c..5f2df67 100644 --- a/db/sqlc/user.sql.go +++ b/db/sqlc/user.sql.go @@ -1,12 +1,13 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: user.sql package db import ( "context" + "database/sql" ) const createUser = `-- name: CreateUser :one @@ -65,3 +66,43 @@ func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { ) return i, err } + +const updateUser = `-- name: UpdateUser :one +UPDATE users +SET + hashed_password = COALESCE($1, hashed_password), + password_changed_at = COALESCE($2, password_changed_at), + full_name = COALESCE($3, full_name), + email = COALESCE($4, email) +WHERE + username = $5 +RETURNING username, hashed_password, full_name, email, password_changed_at, created_at +` + +type UpdateUserParams struct { + HashedPassword sql.NullString + PasswordChangedAt sql.NullTime + FullName sql.NullString + Email sql.NullString + Username string +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateUser, + arg.HashedPassword, + arg.PasswordChangedAt, + arg.FullName, + arg.Email, + arg.Username, + ) + var i User + err := row.Scan( + &i.Username, + &i.HashedPassword, + &i.FullName, + &i.Email, + &i.PasswordChangedAt, + &i.CreatedAt, + ) + return i, err +} diff --git a/db/sqlc/user_test.go b/db/sqlc/user_test.go index f7e8726..27130c0 100644 --- a/db/sqlc/user_test.go +++ b/db/sqlc/user_test.go @@ -2,6 +2,7 @@ package db import ( "context" + "database/sql" "testing" "time" @@ -52,3 +53,95 @@ func TestGetUser(t *testing.T) { require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second) require.WithinDuration(t, user1.PasswordChangedAt, user2.PasswordChangedAt, time.Second) } + +func TestUpdateUserOnlyFullName(t *testing.T) { + oldUser := createRandomUser(t) + + newFullName := util.RandomOwner() + updatedUser, err := testQueries.UpdateUser(context.Background(), UpdateUserParams{ + Username: oldUser.Username, + FullName: sql.NullString{ + String: newFullName, + Valid: true, + }, + }) + + require.NoError(t, err) + require.NotEqual(t, oldUser.FullName, updatedUser.FullName) + require.Equal(t, newFullName, updatedUser.FullName) + require.Equal(t, oldUser.Email, updatedUser.Email) + require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword) +} + +func TestUpdateUserOnlyEmail(t *testing.T) { + oldUser := createRandomUser(t) + + newEmail := util.RandomEmail() + updatedUser, err := testQueries.UpdateUser(context.Background(), UpdateUserParams{ + Username: oldUser.Username, + Email: sql.NullString{ + String: newEmail, + Valid: true, + }, + }) + + require.NoError(t, err) + require.NotEqual(t, oldUser.Email, updatedUser.Email) + require.Equal(t, newEmail, updatedUser.Email) + require.Equal(t, oldUser.FullName, updatedUser.FullName) + require.Equal(t, oldUser.HashedPassword, updatedUser.HashedPassword) +} + +func TestUpdateUserOnlyPassword(t *testing.T) { + oldUser := createRandomUser(t) + + newHashedPassword, err := util.HashPassword(util.RandString(12)) + require.NoError(t, err) + + updatedUser, err := testQueries.UpdateUser(context.Background(), UpdateUserParams{ + Username: oldUser.Username, + HashedPassword: sql.NullString{ + String: newHashedPassword, + Valid: true, + }, + }) + + require.NoError(t, err) + require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword) + require.Equal(t, newHashedPassword, updatedUser.HashedPassword) + require.Equal(t, oldUser.FullName, updatedUser.FullName) + require.Equal(t, oldUser.Email, updatedUser.Email) +} + +func TestUpdateUserAllFields(t *testing.T) { + oldUser := createRandomUser(t) + + newFullName := util.RandomOwner() + newEmail := util.RandomEmail() + newHashedPassword, err := util.HashPassword(util.RandString(12)) + require.NoError(t, err) + + updatedUser, err := testQueries.UpdateUser(context.Background(), UpdateUserParams{ + Username: oldUser.Username, + FullName: sql.NullString{ + String: newFullName, + Valid: true, + }, + Email: sql.NullString{ + String: newEmail, + Valid: true, + }, + HashedPassword: sql.NullString{ + String: newHashedPassword, + Valid: true, + }, + }) + + require.NoError(t, err) + require.NotEqual(t, oldUser.FullName, updatedUser.FullName) + require.Equal(t, newFullName, updatedUser.FullName) + require.NotEqual(t, oldUser.Email, updatedUser.Email) + require.Equal(t, newEmail, updatedUser.Email) + require.NotEqual(t, oldUser.HashedPassword, updatedUser.HashedPassword) + require.Equal(t, newHashedPassword, updatedUser.HashedPassword) +} diff --git a/doc/swagger/bank_of_asia.swagger.json b/doc/swagger/bank_of_asia.swagger.json index 2094444..600809c 100644 --- a/doc/swagger/bank_of_asia.swagger.json +++ b/doc/swagger/bank_of_asia.swagger.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "Bank of Asia API", - "version": "1.0", + "version": "1.1", "contact": { "name": "Hyeonjung Ko", "url": "https://github.com/hyeonjungko/bankofasia", @@ -84,6 +84,38 @@ "BankOfAsia" ] } + }, + "/v1/update_user": { + "patch": { + "operationId": "BankOfAsia_UpdateUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbUpdateUserResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbUpdateUserRequest" + } + } + ], + "tags": [ + "BankOfAsia" + ] + } } }, "definitions": { @@ -148,6 +180,31 @@ } } }, + "pbUpdateUserRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "pbUpdateUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/pbUser" + } + } + }, "pbUser": { "type": "object", "properties": { diff --git a/gapi/authorization.go b/gapi/authorization.go new file mode 100644 index 0000000..66cb2e5 --- /dev/null +++ b/gapi/authorization.go @@ -0,0 +1,46 @@ +package gapi + +import ( + "context" + "fmt" + "strings" + + "github.com/hyeonjungko/bankofasia/token" + "google.golang.org/grpc/metadata" +) + +const ( + authorizationHeader = "authorization" + authorizationBearer = "bearer" +) + +func (server *Server) authorizeUser(ctx context.Context) (*token.Payload, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, fmt.Errorf("missing metadata") + } + + values := md.Get(authorizationHeader) + if len(values) == 0 { + return nil, fmt.Errorf("missing authorization header") + } + + authHeader := values[0] + fields := strings.Fields(authHeader) + if len(fields) < 2 { + return nil, fmt.Errorf("invalid authorization header format") + } + + authType := strings.ToLower(fields[0]) + if authType != authorizationBearer { + return nil, fmt.Errorf("unsupported authorization type: %s", authType) + } + + accessToken := fields[1] + payload, err := server.tokenMaker.VerifyToken(accessToken) + if err != nil { + return nil, fmt.Errorf("invalid access token: %s", err) + } + + return payload, nil +} diff --git a/gapi/error.go b/gapi/error.go index 943691f..b3b6db5 100644 --- a/gapi/error.go +++ b/gapi/error.go @@ -24,3 +24,7 @@ func invalidArgumentError(violations []*errdetails.BadRequest_FieldViolation) er return statusDetails.Err() } + +func unauthenticatedError(err error) error { + return status.Errorf(codes.Unauthenticated, "unauthorized: %s", err) +} diff --git a/gapi/logger.go b/gapi/logger.go new file mode 100644 index 0000000..e98d742 --- /dev/null +++ b/gapi/logger.go @@ -0,0 +1,86 @@ +package gapi + +import ( + "context" + "net/http" + "time" + + "github.com/rs/zerolog/log" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func GrpcLogger( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (resp interface{}, err error) { + startTime := time.Now() + result, err := handler(ctx, req) + duration := time.Since(startTime) + + statusCode := codes.Unknown + if st, ok := status.FromError(err); ok { + statusCode = st.Code() + } + + logger := log.Info() + if err != nil { + logger = log.Error().Err(err) + } + + logger.Str("protocol", "grpc"). + Str("method", info.FullMethod). + Int("status_code", int(statusCode)). + Str("status_text", statusCode.String()). + Dur("duration", duration). + Msg("received a gRPC request") + + return result, err +} + +type ResponseRecorder struct { + http.ResponseWriter + StatusCode int + Body []byte +} + +func (rec *ResponseRecorder) WriteHeader(statusCode int) { + rec.StatusCode = statusCode + rec.ResponseWriter.WriteHeader(statusCode) +} + +func (rec *ResponseRecorder) Write(body []byte) (int, error) { + rec.Body = body + return rec.ResponseWriter.Write(body) +} + +func HttpLogger( + handler http.Handler, +) http.Handler { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + startTime := time.Now() + rec := &ResponseRecorder{ + ResponseWriter: res, + StatusCode: http.StatusOK, + } + handler.ServeHTTP(rec, req) + duration := time.Since(startTime) + + logger := log.Info() + if rec.StatusCode != http.StatusOK { + logger = log.Error().Bytes("body", rec.Body) + } + + logger.Str("protocol", "http"). + Str("method", req.Method). + Str("path", req.RequestURI). + Int("status_code", rec.StatusCode). + Str("status_text", http.StatusText(rec.StatusCode)). + Dur("duration", duration). + Msg("received a HTTP request") + }) +} diff --git a/gapi/rpc_create_user.go b/gapi/rpc_create_user.go index 83c0057..0114bc3 100644 --- a/gapi/rpc_create_user.go +++ b/gapi/rpc_create_user.go @@ -2,11 +2,14 @@ package gapi import ( "context" + "time" + "github.com/hibiken/asynq" db "github.com/hyeonjungko/bankofasia/db/sqlc" "github.com/hyeonjungko/bankofasia/pb" "github.com/hyeonjungko/bankofasia/util" "github.com/hyeonjungko/bankofasia/val" + "github.com/hyeonjungko/bankofasia/worker" "github.com/lib/pq" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" @@ -25,14 +28,29 @@ func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) return nil, status.Errorf(codes.Internal, "failed to hash password: %s", err) } - arg := db.CreateUserParams{ - Username: req.GetUsername(), - HashedPassword: hashedPassword, - FullName: req.GetFullName(), - Email: req.GetEmail(), + arg := db.CreateUserTxParams{ + CreateUserParams: db.CreateUserParams{ + Username: req.GetUsername(), + HashedPassword: hashedPassword, + FullName: req.GetFullName(), + Email: req.GetEmail(), + }, + AfterCreate: func(user db.User) error { + taskPayload := &worker.PayloadSendVerifyEmail{ + Username: user.Username, + } + + opts := []asynq.Option{ + asynq.MaxRetry(10), + asynq.ProcessIn(10 * time.Second), + asynq.Queue(worker.QueueCritical), + } + + return server.taskDistributor.DistributeTaskSendVerifyEmail(ctx, taskPayload, opts...) + }, } - user, err := server.store.CreateUser(ctx, arg) + txResult, err := server.store.CreateUserTx(ctx, arg) if err != nil { if pqErr, ok := err.(*pq.Error); ok { switch pqErr.Code.Name() { @@ -44,7 +62,7 @@ func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) } rsp := &pb.CreateUserResponse{ - User: convertUser(user), + User: convertUser(txResult.User), } return rsp, nil diff --git a/gapi/rpc_update_user.go b/gapi/rpc_update_user.go new file mode 100644 index 0000000..1e46ce1 --- /dev/null +++ b/gapi/rpc_update_user.go @@ -0,0 +1,101 @@ +package gapi + +import ( + "context" + "database/sql" + "time" + + db "github.com/hyeonjungko/bankofasia/db/sqlc" + "github.com/hyeonjungko/bankofasia/pb" + "github.com/hyeonjungko/bankofasia/util" + "github.com/hyeonjungko/bankofasia/val" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (server *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) { + authPayload, err := server.authorizeUser(ctx) + if err != nil { + return nil, unauthenticatedError(err) + } + + violations := validateUpdateUserRequest(req) + if violations != nil { + return nil, invalidArgumentError(violations) + } + + if authPayload.Username != req.GetUsername() { + return nil, status.Errorf(codes.PermissionDenied, "cannot update other user's info") + } + + arg := db.UpdateUserParams{ + Username: req.GetUsername(), + FullName: sql.NullString{ + String: req.GetFullName(), + Valid: req.FullName != nil, + }, + Email: sql.NullString{ + String: req.GetEmail(), + Valid: req.Email != nil, + }, + } + + if req.Password != nil { + // Compute password hash + hashedPassword, err := util.HashPassword(req.GetPassword()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to hash password: %s", err) + } + + arg.HashedPassword = sql.NullString{ + String: hashedPassword, + Valid: true, + } + + arg.PasswordChangedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, + } + } + + user, err := server.store.UpdateUser(ctx, arg) + if err != nil { + if err == sql.ErrNoRows { + return nil, status.Errorf(codes.NotFound, "user not found") + } + return nil, status.Errorf(codes.Internal, "failed to update user: %s", err) + } + + rsp := &pb.UpdateUserResponse{ + User: convertUser(user), + } + + return rsp, nil +} + +func validateUpdateUserRequest(req *pb.UpdateUserRequest) (violations []*errdetails.BadRequest_FieldViolation) { + if err := val.ValidateUsername(req.GetUsername()); err != nil { + violations = append(violations, fieldViolation("username", err)) + } + + if req.Password != nil { + if err := val.ValidatePassword(req.GetPassword()); err != nil { + violations = append(violations, fieldViolation("password", err)) + } + } + + if req.FullName != nil { + if err := val.ValidateFullname(req.GetFullName()); err != nil { + violations = append(violations, fieldViolation("full_name", err)) + } + } + + if req.Email != nil { + if err := val.ValidateEmail(req.GetEmail()); err != nil { + violations = append(violations, fieldViolation("email", err)) + } + } + + return violations +} diff --git a/gapi/server.go b/gapi/server.go index ebd7471..d3b0ada 100644 --- a/gapi/server.go +++ b/gapi/server.go @@ -7,27 +7,30 @@ import ( "github.com/hyeonjungko/bankofasia/pb" "github.com/hyeonjungko/bankofasia/token" "github.com/hyeonjungko/bankofasia/util" + "github.com/hyeonjungko/bankofasia/worker" ) // Server serves gRPC requests type Server struct { pb.UnimplementedBankOfAsiaServer - config util.Config - store db.Store - tokenMaker token.Maker + config util.Config + store db.Store + tokenMaker token.Maker + taskDistributor worker.TaskDistributor } // NewServer creates new gRPC server -func NewServer(config util.Config, store db.Store) (*Server, error) { +func NewServer(config util.Config, store db.Store, taskDistributor worker.TaskDistributor) (*Server, error) { tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey) if err != nil { return nil, fmt.Errorf("cannot create token maker: %w", err) } server := &Server{ - config: config, - store: store, - tokenMaker: tokenMaker, + config: config, + store: store, + tokenMaker: tokenMaker, + taskDistributor: taskDistributor, } return server, nil diff --git a/go.mod b/go.mod index 032ddae..68740d8 100644 --- a/go.mod +++ b/go.mod @@ -9,16 +9,18 @@ require ( github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-migrate/migrate/v4 v4.16.2 github.com/golang/mock v1.6.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 + github.com/hibiken/asynq v0.24.1 github.com/lib/pq v1.10.9 github.com/o1egl/paseto v1.0.0 + github.com/rs/zerolog v1.31.0 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.13.0 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d - google.golang.org/grpc v1.58.0 + google.golang.org/grpc v1.58.2 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 google.golang.org/protobuf v1.31.0 ) @@ -27,8 +29,10 @@ require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -44,13 +48,16 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.3.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -61,8 +68,9 @@ require ( go.uber.org/atomic v1.9.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.4.0 // indirect google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a2b1ecb..9afa9f4 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,10 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= @@ -45,10 +47,16 @@ github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOd github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= @@ -59,9 +67,17 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -89,6 +105,8 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= @@ -123,6 +141,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -137,6 +156,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -154,8 +174,9 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -170,6 +191,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= +github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -192,17 +215,25 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -212,10 +243,20 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -258,6 +299,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -305,6 +348,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -398,11 +442,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -417,6 +464,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -465,6 +514,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -553,8 +604,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= -google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -573,6 +624,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/main.go b/main.go index 711d53c..a4dd647 100644 --- a/main.go +++ b/main.go @@ -5,20 +5,24 @@ import ( "database/sql" "embed" "io/fs" - "log" "net" "net/http" + "os" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/hibiken/asynq" "github.com/hyeonjungko/bankofasia/api" db "github.com/hyeonjungko/bankofasia/db/sqlc" "github.com/hyeonjungko/bankofasia/gapi" "github.com/hyeonjungko/bankofasia/pb" "github.com/hyeonjungko/bankofasia/util" + "github.com/hyeonjungko/bankofasia/worker" _ "github.com/lib/pq" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/encoding/protojson" @@ -35,60 +39,82 @@ func main() { // Load configurations config, err := util.LoadConfig(".") if err != nil { - log.Fatal("Cannot load configurations: ", err) + log.Fatal().Err(err).Msg("Cannot load configurations: ") + } + + if config.Environment == "development" { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } conn, err := sql.Open(config.DBDriver, config.DBSource) if err != nil { - log.Fatal("cannot connect to db:", err) + log.Fatal().Err(err).Msg("cannot connect to db:") } runDBMigration(config.MigrationURL, config.DBSource) store := db.NewStore(conn) - go runGatewayServer(config, store) // run HTTP gateway server in a separate routine - runGrpcServer(config, store) + + redisOpt := asynq.RedisClientOpt{ + Addr: config.RedisAddress, + } + + taskDistributor := worker.NewRedisTaskDistributor(redisOpt) + go runTaskProcessor(redisOpt, store) + + go runGatewayServer(config, store, taskDistributor) // run HTTP gateway server in a separate routine + runGrpcServer(config, store, taskDistributor) } func runDBMigration(migrationURL string, dbSource string) { migration, err := migrate.New(migrationURL, dbSource) if err != nil { - log.Fatal("cannot create new migrate instance: ", err) + log.Fatal().Err(err).Msg("cannot create new migrate instance: ") } if err = migration.Up(); err != nil && err != migrate.ErrNoChange { - log.Fatal("failed to run migrate up: ", err) + log.Fatal().Err(err).Msg("failed to run migrate up: ") } - log.Println("db migrated successfully") + log.Info().Msg("db migrated successfully") +} + +func runTaskProcessor(redisOpt asynq.RedisClientOpt, store db.Store) { + taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store) + log.Info().Msg("start task processor") + err := taskProcessor.Start() + if err != nil { + log.Fatal().Err(err).Msg("failed to start task processor") + } } -func runGrpcServer(config util.Config, store db.Store) { - server, err := gapi.NewServer(config, store) +func runGrpcServer(config util.Config, store db.Store, taskDistributor worker.TaskDistributor) { + server, err := gapi.NewServer(config, store, taskDistributor) if err != nil { - log.Fatal("cannot create server: ", err) + log.Fatal().Err(err).Msg("cannot create server: ") } - grpcServer := grpc.NewServer() + grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger) + grpcServer := grpc.NewServer(grpcLogger) pb.RegisterBankOfAsiaServer(grpcServer, server) reflection.Register(grpcServer) listener, err := net.Listen("tcp", config.GRPCServerAddress) if err != nil { - log.Fatal("cannot create listener:", err) + log.Fatal().Err(err).Msg("cannot create listener:") } - log.Printf("starting gRPC server at %s", listener.Addr().String()) + log.Info().Msgf("starting gRPC server at %s", listener.Addr().String()) err = grpcServer.Serve(listener) if err != nil { - log.Fatal("cannot start gRPC server: ", err) + log.Fatal().Err(err).Msg("cannot start gRPC server: ") } } -func runGatewayServer(config util.Config, store db.Store) { - server, err := gapi.NewServer(config, store) +func runGatewayServer(config util.Config, store db.Store, taskDistributor worker.TaskDistributor) { + server, err := gapi.NewServer(config, store, taskDistributor) if err != nil { - log.Fatal("cannot create server: ", err) + log.Fatal().Err(err).Msg("cannot create server: ") } jsonOption := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ @@ -106,7 +132,7 @@ func runGatewayServer(config util.Config, store db.Store) { err = pb.RegisterBankOfAsiaHandlerServer(ctx, grpcMux, server) if err != nil { - log.Fatal("cannot register handler server: ", err) + log.Fatal().Err(err).Msg("cannot register handler server: ") } mux := http.NewServeMux() @@ -114,7 +140,7 @@ func runGatewayServer(config util.Config, store db.Store) { subFS, err := fs.Sub(swaggerFiles, "doc/swagger") if err != nil { - log.Fatal("cannot create FS from subtree: ", err) + log.Fatal().Err(err).Msg("cannot create FS from subtree: ") } swaggerFS := http.FS(subFS) fs := http.FileServer(swaggerFS) @@ -123,24 +149,25 @@ func runGatewayServer(config util.Config, store db.Store) { listener, err := net.Listen("tcp", config.HTTPServerAddress) if err != nil { - log.Fatal("cannot create listener: ", err) + log.Fatal().Err(err).Msg("cannot create listener: ") } - log.Printf("starting HTTP gateway server at %s", listener.Addr().String()) - err = http.Serve(listener, mux) + log.Info().Msgf("starting HTTP gateway server at %s", listener.Addr().String()) + handler := gapi.HttpLogger(mux) + err = http.Serve(listener, handler) if err != nil { - log.Fatal("cannot start HTTP gateway server: ", err) + log.Fatal().Err(err).Msg("cannot start HTTP gateway server: ") } } func runGinHttpServer(config util.Config, store db.Store) { server, err := api.NewServer(config, store) if err != nil { - log.Fatal("cannot create server: ", err) + log.Fatal().Err(err).Msg("cannot create server: ") } err = server.Start(config.HTTPServerAddress) if err != nil { - log.Fatal("cannot start server:", err) + log.Fatal().Err(err).Msg("cannot start server:") } } diff --git a/pb/rpc_update_user.pb.go b/pb/rpc_update_user.pb.go new file mode 100644 index 0000000..0c699ac --- /dev/null +++ b/pb/rpc_update_user.pb.go @@ -0,0 +1,244 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.24.3 +// source: rpc_update_user.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type UpdateUserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + FullName *string `protobuf:"bytes,2,opt,name=full_name,json=fullName,proto3,oneof" json:"full_name,omitempty"` + Email *string `protobuf:"bytes,3,opt,name=email,proto3,oneof" json:"email,omitempty"` + Password *string `protobuf:"bytes,4,opt,name=password,proto3,oneof" json:"password,omitempty"` +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_update_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_update_user_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_rpc_update_user_proto_rawDescGZIP(), []int{0} +} + +func (x *UpdateUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UpdateUserRequest) GetFullName() string { + if x != nil && x.FullName != nil { + return *x.FullName + } + return "" +} + +func (x *UpdateUserRequest) GetEmail() string { + if x != nil && x.Email != nil { + return *x.Email + } + return "" +} + +func (x *UpdateUserRequest) GetPassword() string { + if x != nil && x.Password != nil { + return *x.Password + } + return "" +} + +type UpdateUserResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *UpdateUserResponse) Reset() { + *x = UpdateUserResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_update_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserResponse) ProtoMessage() {} + +func (x *UpdateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_rpc_update_user_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserResponse.ProtoReflect.Descriptor instead. +func (*UpdateUserResponse) Descriptor() ([]byte, []int) { + return file_rpc_update_user_proto_rawDescGZIP(), []int{1} +} + +func (x *UpdateUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_rpc_update_user_proto protoreflect.FileDescriptor + +var file_rpc_update_user_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x72, 0x70, 0x63, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x1a, 0x0a, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb2, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x09, 0x66, 0x75, 0x6c, + 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, + 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x66, 0x75, 0x6c, 0x6c, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x42, + 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x32, 0x0a, 0x12, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x08, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, + 0x79, 0x65, 0x6f, 0x6e, 0x6a, 0x75, 0x6e, 0x67, 0x6b, 0x6f, 0x2f, 0x62, 0x61, 0x6e, 0x6b, 0x6f, + 0x66, 0x61, 0x73, 0x69, 0x61, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_rpc_update_user_proto_rawDescOnce sync.Once + file_rpc_update_user_proto_rawDescData = file_rpc_update_user_proto_rawDesc +) + +func file_rpc_update_user_proto_rawDescGZIP() []byte { + file_rpc_update_user_proto_rawDescOnce.Do(func() { + file_rpc_update_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_rpc_update_user_proto_rawDescData) + }) + return file_rpc_update_user_proto_rawDescData +} + +var file_rpc_update_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_rpc_update_user_proto_goTypes = []interface{}{ + (*UpdateUserRequest)(nil), // 0: pb.UpdateUserRequest + (*UpdateUserResponse)(nil), // 1: pb.UpdateUserResponse + (*User)(nil), // 2: pb.User +} +var file_rpc_update_user_proto_depIdxs = []int32{ + 2, // 0: pb.UpdateUserResponse.user:type_name -> pb.User + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_rpc_update_user_proto_init() } +func file_rpc_update_user_proto_init() { + if File_rpc_update_user_proto != nil { + return + } + file_user_proto_init() + if !protoimpl.UnsafeEnabled { + file_rpc_update_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateUserRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_update_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateUserResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_rpc_update_user_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rpc_update_user_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_rpc_update_user_proto_goTypes, + DependencyIndexes: file_rpc_update_user_proto_depIdxs, + MessageInfos: file_rpc_update_user_proto_msgTypes, + }.Build() + File_rpc_update_user_proto = out.File + file_rpc_update_user_proto_rawDesc = nil + file_rpc_update_user_proto_goTypes = nil + file_rpc_update_user_proto_depIdxs = nil +} diff --git a/pb/service_bank_of_asia.pb.go b/pb/service_bank_of_asia.pb.go index 170a851..8ef19a3 100644 --- a/pb/service_bank_of_asia.pb.go +++ b/pb/service_bank_of_asia.pb.go @@ -28,18 +28,25 @@ var file_service_bank_of_asia_proto_rawDesc = []byte{ 0x66, 0x5f, 0x61, 0x73, 0x69, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x1a, 0x15, 0x72, 0x70, 0x63, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x72, 0x70, 0x63, 0x5f, 0x6c, 0x6f, 0x67, - 0x69, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, - 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xba, 0x01, 0x0a, 0x0a, - 0x42, 0x61, 0x6e, 0x6b, 0x4f, 0x66, 0x41, 0x73, 0x69, 0x61, 0x12, 0x57, 0x0a, 0x0a, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x69, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x72, + 0x70, 0x63, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, + 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x32, 0x93, 0x02, 0x0a, 0x0a, 0x42, 0x61, 0x6e, 0x6b, 0x4f, 0x66, 0x41, 0x73, 0x69, + 0x61, 0x12, 0x57, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x57, 0x0a, 0x0a, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, - 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x75, + 0x01, 0x2a, 0x32, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x14, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x69, @@ -52,7 +59,7 @@ var file_service_bank_of_asia_proto_rawDesc = []byte{ 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x79, 0x65, 0x6f, 0x6e, 0x6a, 0x75, 0x6e, 0x67, 0x6b, 0x6f, 0x2f, 0x62, 0x61, 0x6e, 0x6b, 0x6f, 0x66, 0x61, 0x73, 0x69, 0x61, 0x1a, 0x12, 0x68, 0x6a, 0x6b, 0x6f, 0x31, 0x32, 0x30, 0x33, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, - 0x6d, 0x32, 0x03, 0x31, 0x2e, 0x30, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6d, 0x32, 0x03, 0x31, 0x2e, 0x31, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x79, 0x65, 0x6f, 0x6e, 0x6a, 0x75, 0x6e, 0x67, 0x6b, 0x6f, 0x2f, 0x62, 0x61, 0x6e, 0x6b, 0x6f, 0x66, 0x61, 0x73, 0x69, 0x61, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, @@ -60,17 +67,21 @@ var file_service_bank_of_asia_proto_rawDesc = []byte{ var file_service_bank_of_asia_proto_goTypes = []interface{}{ (*CreateUserRequest)(nil), // 0: pb.CreateUserRequest - (*LoginUserRequest)(nil), // 1: pb.LoginUserRequest - (*CreateUserResponse)(nil), // 2: pb.CreateUserResponse - (*LoginUserResponse)(nil), // 3: pb.LoginUserResponse + (*UpdateUserRequest)(nil), // 1: pb.UpdateUserRequest + (*LoginUserRequest)(nil), // 2: pb.LoginUserRequest + (*CreateUserResponse)(nil), // 3: pb.CreateUserResponse + (*UpdateUserResponse)(nil), // 4: pb.UpdateUserResponse + (*LoginUserResponse)(nil), // 5: pb.LoginUserResponse } var file_service_bank_of_asia_proto_depIdxs = []int32{ 0, // 0: pb.BankOfAsia.CreateUser:input_type -> pb.CreateUserRequest - 1, // 1: pb.BankOfAsia.LoginUser:input_type -> pb.LoginUserRequest - 2, // 2: pb.BankOfAsia.CreateUser:output_type -> pb.CreateUserResponse - 3, // 3: pb.BankOfAsia.LoginUser:output_type -> pb.LoginUserResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type + 1, // 1: pb.BankOfAsia.UpdateUser:input_type -> pb.UpdateUserRequest + 2, // 2: pb.BankOfAsia.LoginUser:input_type -> pb.LoginUserRequest + 3, // 3: pb.BankOfAsia.CreateUser:output_type -> pb.CreateUserResponse + 4, // 4: pb.BankOfAsia.UpdateUser:output_type -> pb.UpdateUserResponse + 5, // 5: pb.BankOfAsia.LoginUser:output_type -> pb.LoginUserResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -83,6 +94,7 @@ func file_service_bank_of_asia_proto_init() { } file_rpc_create_user_proto_init() file_rpc_login_user_proto_init() + file_rpc_update_user_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pb/service_bank_of_asia.pb.gw.go b/pb/service_bank_of_asia.pb.gw.go index 80660e9..3a66b26 100644 --- a/pb/service_bank_of_asia.pb.gw.go +++ b/pb/service_bank_of_asia.pb.gw.go @@ -65,6 +65,40 @@ func local_request_BankOfAsia_CreateUser_0(ctx context.Context, marshaler runtim } +func request_BankOfAsia_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, client BankOfAsiaClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateUserRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.UpdateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_BankOfAsia_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, server BankOfAsiaServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateUserRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.UpdateUser(ctx, &protoReq) + return msg, metadata, err + +} + func request_BankOfAsia_LoginUser_0(ctx context.Context, marshaler runtime.Marshaler, client BankOfAsiaClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LoginUserRequest var metadata runtime.ServerMetadata @@ -130,6 +164,31 @@ func RegisterBankOfAsiaHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("PATCH", pattern_BankOfAsia_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.BankOfAsia/UpdateUser", runtime.WithHTTPPathPattern("/v1/update_user")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_BankOfAsia_UpdateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_BankOfAsia_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_BankOfAsia_LoginUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -218,6 +277,28 @@ func RegisterBankOfAsiaHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("PATCH", pattern_BankOfAsia_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/pb.BankOfAsia/UpdateUser", runtime.WithHTTPPathPattern("/v1/update_user")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_BankOfAsia_UpdateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_BankOfAsia_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_BankOfAsia_LoginUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -246,11 +327,15 @@ func RegisterBankOfAsiaHandlerClient(ctx context.Context, mux *runtime.ServeMux, var ( pattern_BankOfAsia_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "create_user"}, "")) + pattern_BankOfAsia_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "update_user"}, "")) + pattern_BankOfAsia_LoginUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "login_user"}, "")) ) var ( forward_BankOfAsia_CreateUser_0 = runtime.ForwardResponseMessage + forward_BankOfAsia_UpdateUser_0 = runtime.ForwardResponseMessage + forward_BankOfAsia_LoginUser_0 = runtime.ForwardResponseMessage ) diff --git a/pb/service_bank_of_asia_grpc.pb.go b/pb/service_bank_of_asia_grpc.pb.go index 79989d5..11b7e2a 100644 --- a/pb/service_bank_of_asia_grpc.pb.go +++ b/pb/service_bank_of_asia_grpc.pb.go @@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion7 const ( BankOfAsia_CreateUser_FullMethodName = "/pb.BankOfAsia/CreateUser" + BankOfAsia_UpdateUser_FullMethodName = "/pb.BankOfAsia/UpdateUser" BankOfAsia_LoginUser_FullMethodName = "/pb.BankOfAsia/LoginUser" ) @@ -28,6 +29,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type BankOfAsiaClient interface { CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) + UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) LoginUser(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error) } @@ -48,6 +50,15 @@ func (c *bankOfAsiaClient) CreateUser(ctx context.Context, in *CreateUserRequest return out, nil } +func (c *bankOfAsiaClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) { + out := new(UpdateUserResponse) + err := c.cc.Invoke(ctx, BankOfAsia_UpdateUser_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *bankOfAsiaClient) LoginUser(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error) { out := new(LoginUserResponse) err := c.cc.Invoke(ctx, BankOfAsia_LoginUser_FullMethodName, in, out, opts...) @@ -62,6 +73,7 @@ func (c *bankOfAsiaClient) LoginUser(ctx context.Context, in *LoginUserRequest, // for forward compatibility type BankOfAsiaServer interface { CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) + UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) mustEmbedUnimplementedBankOfAsiaServer() } @@ -73,6 +85,9 @@ type UnimplementedBankOfAsiaServer struct { func (UnimplementedBankOfAsiaServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") } +func (UnimplementedBankOfAsiaServer) UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") +} func (UnimplementedBankOfAsiaServer) LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LoginUser not implemented") } @@ -107,6 +122,24 @@ func _BankOfAsia_CreateUser_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _BankOfAsia_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BankOfAsiaServer).UpdateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: BankOfAsia_UpdateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BankOfAsiaServer).UpdateUser(ctx, req.(*UpdateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _BankOfAsia_LoginUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LoginUserRequest) if err := dec(in); err != nil { @@ -136,6 +169,10 @@ var BankOfAsia_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateUser", Handler: _BankOfAsia_CreateUser_Handler, }, + { + MethodName: "UpdateUser", + Handler: _BankOfAsia_UpdateUser_Handler, + }, { MethodName: "LoginUser", Handler: _BankOfAsia_LoginUser_Handler, diff --git a/proto/rpc_update_user.proto b/proto/rpc_update_user.proto new file mode 100644 index 0000000..aebfced --- /dev/null +++ b/proto/rpc_update_user.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package pb; + +import "user.proto"; + +option go_package = "github.com/hyeonjungko/bankofasia/pb"; + +message UpdateUserRequest { + string username = 1; + optional string full_name = 2; + optional string email = 3; + optional string password = 4; +} + +message UpdateUserResponse { + User user = 1; +} \ No newline at end of file diff --git a/proto/service_bank_of_asia.proto b/proto/service_bank_of_asia.proto index 45340f9..9288627 100644 --- a/proto/service_bank_of_asia.proto +++ b/proto/service_bank_of_asia.proto @@ -4,6 +4,7 @@ package pb; import "rpc_create_user.proto"; import "rpc_login_user.proto"; +import "rpc_update_user.proto"; import "google/api/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; @@ -12,7 +13,7 @@ option go_package = "github.com/hyeonjungko/bankofasia/pb"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Bank of Asia API"; - version: "1.0"; + version: "1.1"; contact: { name: "Hyeonjung Ko"; url: "https://github.com/hyeonjungko/bankofasia"; @@ -28,10 +29,16 @@ service BankOfAsia { body: "*" }; } - rpc LoginUser (LoginUserRequest) returns (LoginUserResponse) { + rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse) { option (google.api.http) = { - post: "/v1/login_user" + patch: "/v1/update_user" body: "*" }; } + rpc LoginUser (LoginUserRequest) returns (LoginUserResponse) { + option (google.api.http) = { + post: "/v1/login_user" + body: "*" + }; + } } \ No newline at end of file diff --git a/util/config.go b/util/config.go index e8ee403..45ab017 100644 --- a/util/config.go +++ b/util/config.go @@ -9,9 +9,11 @@ import ( // Config stores all configurations of the application // The values are read by Viper form a config file or environmental variables. type Config struct { + Environment string `mapstructure:"ENVIRONMENT"` DBDriver string `mapstructure:"DB_DRIVER"` DBSource string `mapstructure:"DB_SOURCE"` MigrationURL string `mapstructure:"MIGRATION_URL"` + RedisAddress string `mapstructure:"REDIS_ADDRESS"` HTTPServerAddress string `mapstructure:"HTTP_SERVER_ADDRESS"` GRPCServerAddress string `mapstructure:"GRPC_SERVER_ADDRESS"` TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` diff --git a/val/validator.go b/val/validator.go index aef6f61..efe5279 100644 --- a/val/validator.go +++ b/val/validator.go @@ -8,7 +8,7 @@ import ( var ( isValidUsername = regexp.MustCompile(`^[a-z0-9_]+$`).MatchString - isValidFullname = regexp.MustCompile(`^[a-zA-\\s]+$`).MatchString + isValidFullname = regexp.MustCompile(`^[a-zA-Z\s]+$`).MatchString ) func ValidateString(value string, minLength int, maxLength int) error { @@ -20,7 +20,7 @@ func ValidateString(value string, minLength int, maxLength int) error { } func ValidateUsername(value string) error { - if err := ValidateString(value, 3, 100); err != nil { + if err := ValidateString(value, 2, 100); err != nil { return err } diff --git a/worker/distributor.go b/worker/distributor.go new file mode 100644 index 0000000..d306083 --- /dev/null +++ b/worker/distributor.go @@ -0,0 +1,26 @@ +package worker + +import ( + "context" + + "github.com/hibiken/asynq" +) + +type TaskDistributor interface { + DistributeTaskSendVerifyEmail( + ctx context.Context, + payload *PayloadSendVerifyEmail, + opts ...asynq.Option, + ) error +} + +type RedisTaskDistributor struct { + client *asynq.Client +} + +func NewRedisTaskDistributor(redisOpt asynq.RedisClientOpt) TaskDistributor { + client := asynq.NewClient(redisOpt) + return &RedisTaskDistributor{ + client: client, + } +} diff --git a/worker/processor.go b/worker/processor.go new file mode 100644 index 0000000..89ad24b --- /dev/null +++ b/worker/processor.go @@ -0,0 +1,48 @@ +package worker + +import ( + "context" + + "github.com/hibiken/asynq" + db "github.com/hyeonjungko/bankofasia/db/sqlc" +) + +const ( + QueueCritical = "critical" + QueueDefault = "default" +) + +type TaskProcessor interface { + Start() error + ProcessTaskSendVerifyEmail(ctx context.Context, task *asynq.Task) error +} + +type RedisTaskProcessor struct { + server *asynq.Server + store db.Store +} + +func NewRedisTaskProcessor(redisOpt asynq.RedisClientOpt, store db.Store) TaskProcessor { + server := asynq.NewServer( + redisOpt, + asynq.Config{ + Queues: map[string]int{ + QueueCritical: 10, + QueueDefault: 5, + }, + }, + ) + + return &RedisTaskProcessor{ + server: server, + store: store, + } +} + +func (processor *RedisTaskProcessor) Start() error { + mux := asynq.NewServeMux() + + mux.HandleFunc(TaskSendVerifyEmail, processor.ProcessTaskSendVerifyEmail) + + return processor.server.Start(mux) +} diff --git a/worker/task_send_verify_email.go b/worker/task_send_verify_email.go new file mode 100644 index 0000000..8df23ae --- /dev/null +++ b/worker/task_send_verify_email.go @@ -0,0 +1,69 @@ +package worker + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "github.com/hibiken/asynq" + "github.com/rs/zerolog/log" +) + +const TaskSendVerifyEmail = "task:send_verify_email" + +type PayloadSendVerifyEmail struct { + Username string `json:"username"` +} + +func (distributor *RedisTaskDistributor) DistributeTaskSendVerifyEmail( + ctx context.Context, + payload *PayloadSendVerifyEmail, + opts ...asynq.Option, +) error { + jsonPayload, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal task payload: %w", err) + } + + task := asynq.NewTask(TaskSendVerifyEmail, jsonPayload, opts...) + info, err := distributor.client.EnqueueContext(ctx, task) + if err != nil { + return fmt.Errorf("failed to enqueue task: %w", err) + } + + log.Info(). + Str("type", task.Type()). + Bytes("payload", task.Payload()). + Str("queue", info.Queue). + Int("max_retry", info.MaxRetry). + Msg("enqueued task") + + return nil +} + +func (processor *RedisTaskProcessor) ProcessTaskSendVerifyEmail( + ctx context.Context, + task *asynq.Task, +) error { + var payload PayloadSendVerifyEmail + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + return fmt.Errorf("failed to unmarshal payload: %w", asynq.SkipRetry) + } + + user, err := processor.store.GetUser(ctx, payload.Username) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("user doesn't exist: %w", asynq.SkipRetry) + } + return fmt.Errorf("failed to get user: %w", err) + } + + //TODO: send verification email to users + log.Info().Str("type", task.Type()). + Bytes("payload", task.Payload()). + Str("email", user.Email). + Msg("processed task") + + return nil +}