Skip to content

Commit

Permalink
added gRPC API and gRPC gateway API
Browse files Browse the repository at this point in the history
  • Loading branch information
prosenjitjoy committed Oct 25, 2023
1 parent 84b5745 commit 02a7133
Show file tree
Hide file tree
Showing 88 changed files with 8,579 additions and 125 deletions.
13 changes: 8 additions & 5 deletions .env
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
POD_NAME=simplebank
BE_CONTAINER=prosenjitjoy/simplebank
BE_CONTAINER=simplebank-api
DB_CONTAINER=postgres
DB_NAME=bankdb
DB_USER=postgres
DB_PASS=postgres
DATABASE_URL=postgres://postgres:postgres@localhost:5432/bankdb
MIGRATE_URL=pgx5://postgres:postgres@localhost:5432/bankdb
SERVER_ADDR=:5000
DATABASE_URL=postgres://postgres:postgres@localhost:5432/bankdb?sslmode=disable
MIGRATION_URL=file://database/migration
HTTP_SERVER_ADDR=:3000
GRPC_SERVER_ADDR=:3001
SECRET_KEY=11111111222222223333333344444444
TOKEN_DURATION=15m
TOKEN_DURATION=15m
REFRESH_DURATION=24h
ENVIRONMENT=dev
13 changes: 4 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
FROM golang:alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main cmd/main.go
RUN apk add curl
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz
RUN go build -o main main.go

FROM alpine
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/migrate .
COPY database/migration ./migration
COPY start.sh .
COPY database/migration ./database/migration
COPY .env .

EXPOSE 5000
CMD [ "/app/main" ]
ENTRYPOINT ["/app/start.sh"]
EXPOSE 3000
CMD [ "/app/main" ]
27 changes: 16 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
include .env

create_container:
podman run --name ${CONTAINER_NAME} -e POSTGRES_USER=${DB_USER} -e POSTGRES_PASSWORD=${DB_PASS} -p 5432:5432 -d postgres
podman run --name ${DB_CONTAINER} -e POSTGRES_USER=${DB_USER} -e POSTGRES_PASSWORD=${DB_PASS} -p 5432:5432 -d postgres

create_database:
podman exec -it ${CONTAINER_NAME} createdb --username=${DB_USER} ${DB_NAME}
podman exec -it ${DB_CONTAINER} createdb --username=${DB_USER} ${DB_NAME}

delete_database:
podman exec -it ${CONTAINER_NAME} dropdb --username=${DB_USER} ${DB_NAME}
podman exec -it ${DB_CONTAINER} dropdb --username=${DB_USER} ${DB_NAME}

open_database:
podman exec -it ${CONTAINER_NAME} psql -U ${DB_USER} -d ${DB_NAME}
podman exec -it ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}

create_migration:
migrate create -ext sql -dir database/migration -seq create_tables

migrate_up:
migrate -database ${MIGRATE_URL} -path database/migration -verbose up
migrate -database ${DATABASE_URL} -path database/migration -verbose up

migrate_up_last:
migrate -database ${MIGRATE_URL} -path database/migration -verbose up 1
migrate -database ${DATABASE_URL} -path database/migration -verbose up 1

migrate_down:
migrate -database ${MIGRATE_URL} -path database/migration -verbose down
migrate -database ${DATABASE_URL} -path database/migration -verbose down

migrate_down_last:
migrate -database ${MIGRATE_URL} -path database/migration -verbose down 1
migrate -database ${DATABASE_URL} -path database/migration -verbose down 1

sqlc_generate:
sqlc generate

mock_generate:
mockgen -package mockdb -destination database/mockdb/store.go main/database/db Store

proto_gererate:
rm -rf pb/*.go
rm -rf doc/swagger/*.json
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative --grpc-gateway_out=pb --grpc-gateway_opt=paths=source_relative --openapiv2_out=swagger --openapiv2_opt=allow_merge=true,merge_file_name=simplebank proto/*.proto

run_test:
go test -v -cover ./...

Expand All @@ -42,10 +47,10 @@ run_server:
dev_deploy:
podman pod rm -af
podman rm -af
podman pod create -p 5000:5000 ${POD_NAME}
podman pod create -p 3000:3000 ${POD_NAME}
podman pod start ${POD_NAME}
podman run --pod ${POD_NAME} --name ${DB_CONTAINER} -e POSTGRES_USER=${DB_USER} -e POSTGRES_PASSWORD=${DB_PASS} -e POSTGRES_DB=${DB_NAME} -d postgres
podman build -t ${BE_CONTAINER}:latest .
podman run --pod ${POD_NAME} --name ${BE_CONTAINER_NAME} -e DB_SOURCE=${MIGRATE_URL} ${BE_CONTAINER}:latest
podman run --pod ${POD_NAME} --name ${BE_CONTAINER} -e DB_SOURCE=${MIGRATE_URL} ${BE_CONTAINER}:latest

.PHONY: create_container create_database delete_database open_database create_migration migrate_up migrate_up_last migrate_down migrate_down_last sqlc_generate mock_generate run_test run_server dev_deploy
.PHONY: create_container create_database delete_database open_database create_migration migrate_up migrate_up_last migrate_down migrate_down_last sqlc_generate mock_generate proto_gererate run_test run_server dev_deploy
3 changes: 2 additions & 1 deletion api/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ func addAuthorization(
username string,
duration time.Duration,
) {
token, err := tokenMaker.CreateToken(username, duration)
token, payload, err := tokenMaker.CreateToken(username, duration)
require.NoError(t, err)
require.NotEmpty(t, payload)

authorizationHeader := fmt.Sprintf("%s %s", authorizationType, token)
request.Header.Set(authorizationHeaderKey, authorizationHeader)
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (s *Server) setupRouter() {

router.POST("/users", s.createUser)
router.POST("/users/login", s.loginUser)
router.POST("/tokens/renew", s.renewAccessToken)

authRoutes := router.Group("/")
authRoutes.Use(authMiddleware(s.tokenMaker))
Expand Down
81 changes: 81 additions & 0 deletions api/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package api

import (
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
)

type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}

type renewAccessTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
}

func (s *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessTokenRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

refreshPayload, err := s.tokenMaker.VerifyToken(req.RefreshToken)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

session, err := s.store.GetSession(ctx, refreshPayload.ID)
if err != nil {
if err == pgx.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}

ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

if session.IsBlocked {
err := fmt.Errorf("blocked session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

if session.Username != refreshPayload.Username {
err := fmt.Errorf("incorrect session user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

if session.RefreshToken != req.RefreshToken {
err := fmt.Errorf("mismatched session token")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

if time.Now().After(session.ExpiresAt) {
err := fmt.Errorf("expired session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

accessToken, accessPayload, err := s.tokenMaker.CreateToken(refreshPayload.Username, s.config.TokenDuration)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

response := renewAccessTokenResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
}

ctx.JSON(http.StatusOK, response)
}
39 changes: 34 additions & 5 deletions api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)

Expand Down Expand Up @@ -75,8 +76,12 @@ type loginUserRequest struct {
}

type loginUserResponse struct {
AccessToken string `json:"access_token"`
User *userResponse `json:"user"`
SessionID uuid.UUID `json:"session_id"`
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
User *userResponse `json:"user"`
}

func (s *Server) loginUser(ctx *gin.Context) {
Expand All @@ -103,15 +108,39 @@ func (s *Server) loginUser(ctx *gin.Context) {
return
}

accessToken, err := s.tokenMaker.CreateToken(user.Username, s.config.TokenDuration)
accessToken, accessPayload, err := s.tokenMaker.CreateToken(user.Username, s.config.TokenDuration)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

refreshToken, refreshPayload, err := s.tokenMaker.CreateToken(user.Username, s.config.RefreshDuration)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

session, err := s.store.CreateSession(ctx, &db.CreateSessionParams{
ID: refreshPayload.ID,
Username: user.Username,
RefreshToken: refreshToken,
UserAgent: ctx.Request.UserAgent(),
ClientIp: ctx.ClientIP(),
IsBlocked: false,
ExpiresAt: refreshPayload.ExpiredAt,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

response := loginUserResponse{
AccessToken: accessToken,
User: newUserResponse(user),
SessionID: session.ID,
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
RefreshToken: refreshToken,
RefreshTokenExpiresAt: refreshPayload.ExpiredAt,
User: newUserResponse(user),
}

ctx.JSON(http.StatusOK, response)
Expand Down
108 changes: 108 additions & 0 deletions api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,111 @@ func randomUser(t *testing.T) (*db.User, string) {

return user, password
}

func TestLoginUserAPI(t *testing.T) {
user, password := randomUser(t)

testCases := []struct {
name string
body gin.H
buildStubs func(store *mockdb.MockStore)
checkResponse func(recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
body: gin.H{
"username": user.Username,
"password": password,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
GetUser(gomock.Any(), gomock.Eq(user.Username)).
Times(1).
Return(user, nil)
store.EXPECT().
CreateSession(gomock.Any(), gomock.Any()).
Times(1).Return(&db.Session{}, nil)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
{
name: "UserNotFound",
body: gin.H{
"username": "NotFound",
"password": password,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
GetUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, pgx.ErrNoRows)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusNotFound, recorder.Code)
},
},
{
name: "IncorrectPassword",
body: gin.H{
"username": user.Username,
"password": "incorrect",
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().GetUser(gomock.Any(), gomock.Eq(user.Username)).Times(1).Return(user, nil)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "InternalError",
body: gin.H{
"username": user.Username,
"password": password,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().GetUser(gomock.Any(), gomock.Any()).Times(1).Return(&db.User{}, pgx.ErrTxClosed)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusInternalServerError, recorder.Code)
},
},
{
name: "InvalidUsername",
body: gin.H{
"username": "invalid-user@1",
"password": password,
},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().GetUser(gomock.Any(), gomock.Any()).Times(0)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store)
server := newTestServer(t, store)
recorder := httptest.NewRecorder()

// Marshal body data to JSON
data, err := json.Marshal(tc.body)
require.NoError(t, err)

url := "/users/login"
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err)
server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder)
})
}
}
Loading

0 comments on commit 02a7133

Please sign in to comment.