Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
## Database Design
<img src="image/bank-db-design.png"/>

## migrate commands
```
migrate create -ext sql -dir db/migration -seq add_sessions
```

## Viper
- find, load, unmarshal config file (json, yaml, toml, env, ini)
- read config from env variables
Expand Down
3 changes: 2 additions & 1 deletion api/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import (
)

func addAuthorization(t *testing.T, r *http.Request, tokenMaker token.Maker, authorizationType string, 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)
r.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 @@ -44,6 +44,7 @@ func (server *Server) setUpRouterWithSubUrl() {
router := gin.Default()
router.POST("/users", server.createUser)
router.POST("/users/login", server.loginUser)
router.POST("/tokens/renew_access", server.renewAccessToken)

authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker))
authRoutes.POST("/accounts", server.createAccount)
Expand Down
71 changes: 71 additions & 0 deletions api/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package api

import (
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)

type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type renewAccessTokenResponse struct {
AccessToken string `json:"accessToken"`
AccessTokenExpireAt time.Time `json:"accessTokenExpireAt"`
}

func (server *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessTokenRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
fmt.Println("....asdhf", req.RefreshToken)
refreshPayload, err := server.tokenMaker.VerifyToken(req.RefreshToken)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
session, err := server.store.GetSession(ctx, refreshPayload.ID)
if err != nil {
if err == sql.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("mismatched session token")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(refreshPayload.Username, server.config.AccessTokenDuration)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
resp := renewAccessTokenResponse{
AccessToken: accessToken,
AccessTokenExpireAt: accessPayload.ExpiredAt,
}
ctx.JSON(http.StatusOK, resp)
}
41 changes: 36 additions & 5 deletions api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
db "github.com/emon46/bank-application/db/sqlc"
"github.com/emon46/bank-application/util"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/lib/pq"
"net/http"
"time"
Expand Down Expand Up @@ -71,8 +72,12 @@ type loginUserRequest struct {
Password string `json:"password" binding:"required,min=6"`
}
type loginUserResponse struct {
AccessToken string `json:"accessToken"`
User userResponse `json:"user"`
SessionID uuid.UUID `json:"sessionID"`
AccessToken string `json:"accessToken"`
AccessTokenExpireAt time.Time `json:"accessTokenExpireAt"`
RefreshToken string `json:"refreshToken"`
RefreshTokenExpireAt time.Time `json:"refreshTokenExpireAt"`
User userResponse `json:"user"`
}

func (server *Server) loginUser(ctx *gin.Context) {
Expand All @@ -88,20 +93,46 @@ func (server *Server) loginUser(ctx *gin.Context) {
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
err = util.CheckPassword(req.Password, user.HashedPassword)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
accessToken, err := server.tokenMaker.CreateToken(user.Username, server.config.AccessTokenDuration)
accessToken, accessPayload, err := server.tokenMaker.CreateToken(user.Username, server.config.AccessTokenDuration)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

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

session, err := server.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
}

resp := loginUserResponse{
AccessToken: accessToken,
User: newUserResponse(user),
SessionID: session.ID,
AccessToken: accessToken,
AccessTokenExpireAt: accessPayload.ExpiredAt,
RefreshToken: refreshToken,
RefreshTokenExpireAt: refreshPayload.ExpiredAt,
User: newUserResponse(user),
}
ctx.JSON(http.StatusOK, resp)
}
4 changes: 4 additions & 0 deletions api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ func TestLoginUserAPI(t *testing.T) {
GetUser(gomock.Any(), gomock.Eq(user.Username)).
Times(1).
Return(user, nil)
store.EXPECT().
CreateSession(gomock.Any(), gomock.Any()).
Times(1)

},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
Expand Down
1 change: 1 addition & 0 deletions app.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ DB_SOURCE="postgresql://postgres:secret1234@localhost:5432/simple_bank?sslmode=d
SERVER_ADDRESS="0.0.0.0:8080"
TOKEN_SYMMETRIC_KEY="12345678123456781234567812345678"
ACESS_TOKEN_DURATION=15m
REFRESH_TOKEN_DURATION=24h
28 changes: 14 additions & 14 deletions db/migration/000001_init_schema.up.sql
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
CREATE TABLE "account" (
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);

CREATE TABLE "entries" (
"id" bigserial PRIMARY KEY,
"account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
"id" bigserial PRIMARY KEY,
"account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);

CREATE TABLE "transfers" (
"id" bigserial PRIMARY KEY,
"from_account_id" bigint NOT NULL,
"to_account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
"id" bigserial PRIMARY KEY,
"from_account_id" bigint NOT NULL,
"to_account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);

CREATE INDEX ON "account" ("owner");
Expand Down
12 changes: 6 additions & 6 deletions db/migration/000002_add_users.up.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"password_changed_at" timestamptz NOT NULL DEFAULT ('0001-01-01T00:00:00Z')
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"password_changed_at" timestamptz NOT NULL DEFAULT ('0001-01-01T00:00:00Z')
);

ALTER TABLE "account" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
Expand Down
1 change: 1 addition & 0 deletions db/migration/000003_add_sessions.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS "sessions";
12 changes: 12 additions & 0 deletions db/migration/000003_add_sessions.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY,
"username" varchar NOT NULL,
"refresh_token" varchar NOT NULL,
"user_agent" varchar NOT NULL,
"client_ip" varchar NOT NULL,
"is_blocked" boolean NOT NULL DEFAULT FALSE,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"expires_at" timestamptz NOT NULL
);

ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
31 changes: 31 additions & 0 deletions db/mock/store.go

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

16 changes: 16 additions & 0 deletions db/query/session.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- name: CreateSession :one
INSERT INTO sessions (
id,
username,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
) RETURNING *;

-- name: GetSession :one
SELECT * FROM sessions
WHERE id = $1 LIMIT 1;
13 changes: 13 additions & 0 deletions db/sqlc/models.go

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

4 changes: 4 additions & 0 deletions db/sqlc/querier.go

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

Loading