Skip to content

Commit a5deb00

Browse files
committed
😇 add session
1 parent 58ba492 commit a5deb00

23 files changed

+392
-28
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
# tinybank
2+
23
The process of learning about golang
4+
5+
- Create a new db migration:
6+
7+
```bash
8+
migrate create -ext sql -dir db/migrations -seq add_sessions
9+
```

api/middleware_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ func addAuthorization(
2020
username string,
2121
duration time.Duration,
2222
) {
23-
token, err := tokenMaker.CreateToken(username, duration)
23+
token, payload, err := tokenMaker.CreateToken(username, duration)
2424
require.NoError(t, err)
25+
require.NotEmpty(t, payload)
2526

2627
authorizationHeader := fmt.Sprintf("%s %s", authorizationType, token)
2728
request.Header.Set(authorizationHeaderKey, authorizationHeader)

api/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func NewServer(config utils.Config, store db.Store) (*Server, error) {
3737
func (server *Server) setupRouter() {
3838
router := gin.Default()
3939

40+
router.POST("/auth/refresh_token", server.renewAccessToken)
41+
4042
router.POST("/users", server.createUser)
4143
router.POST("/users/login", server.loginUser)
4244

api/token.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package api
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
type renewAccessTokenRequest struct {
13+
RefreshToken string `json:"refresh_token" binding:"required"`
14+
}
15+
16+
type renewAccessTokenResponse struct {
17+
AccessToken string `json:"access_token"`
18+
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
19+
}
20+
21+
func (server *Server) renewAccessToken(ctx *gin.Context) {
22+
var req renewAccessTokenRequest
23+
if err := ctx.ShouldBindJSON(&req); err != nil {
24+
ctx.JSON(http.StatusBadRequest, errorResponse(err))
25+
return
26+
}
27+
28+
refreshPayload, err := server.tokenMaker.VerifyToken(req.RefreshToken)
29+
if err != nil {
30+
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
31+
return
32+
}
33+
34+
session, err := server.store.GetSession(ctx, refreshPayload.ID)
35+
if err != nil {
36+
if err == sql.ErrNoRows {
37+
ctx.JSON(http.StatusNotFound, errorResponse(err))
38+
return
39+
}
40+
41+
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
42+
return
43+
}
44+
45+
if session.IsBlocked {
46+
err := fmt.Errorf("session %d is blocked", session.ID)
47+
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
48+
return
49+
}
50+
51+
if session.Username != refreshPayload.Username {
52+
err := fmt.Errorf("session %d does not belong to user %s", session.ID, refreshPayload.Username)
53+
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
54+
return
55+
}
56+
57+
if session.RefreshToken != req.RefreshToken {
58+
err := fmt.Errorf("refresh token does not match")
59+
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
60+
return
61+
}
62+
63+
if time.Now().After(session.ExpiresAt) {
64+
err := fmt.Errorf("session %d is expired", session.ID)
65+
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
66+
return
67+
}
68+
69+
accessToken, accessPayload, err := server.tokenMaker.CreateToken(refreshPayload.Username, server.config.AccessTokenDuration)
70+
if err != nil {
71+
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
72+
return
73+
}
74+
75+
resp := renewAccessTokenResponse{
76+
AccessToken: accessToken,
77+
AccessTokenExpiresAt: accessPayload.ExpiredAt,
78+
}
79+
80+
ctx.JSON(http.StatusOK, resp)
81+
}

api/user.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/gin-gonic/gin"
9+
"github.com/google/uuid"
910
"github.com/lib/pq"
1011
db "github.com/nc-minh/tinybank/db/sqlc"
1112
"github.com/nc-minh/tinybank/utils"
@@ -79,8 +80,12 @@ type loginUserRequest struct {
7980
}
8081

8182
type loginUserResponse struct {
82-
AccessToken string `json:"access_token"`
83-
User userResponse `json:"user"`
83+
SessionID uuid.UUID `json:"session_id"`
84+
AccessToken string `json:"access_token"`
85+
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
86+
RefreshToken string `json:"refresh_token"`
87+
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
88+
User userResponse `json:"user"`
8489
}
8590

8691
func (server *Server) loginUser(ctx *gin.Context) {
@@ -107,15 +112,42 @@ func (server *Server) loginUser(ctx *gin.Context) {
107112
return
108113
}
109114

110-
accessToken, err := server.tokenMaker.CreateToken(user.Username, server.config.AccessTokenDuration)
115+
accessToken, accessPayload, err := server.tokenMaker.CreateToken(user.Username, server.config.AccessTokenDuration)
116+
if err != nil {
117+
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
118+
return
119+
}
120+
121+
refreshToken, refreshPayload, err := server.tokenMaker.CreateToken(
122+
user.Username,
123+
server.config.RefreshTokenDuration,
124+
)
125+
if err != nil {
126+
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
127+
return
128+
}
129+
130+
session, err := server.store.CreateSession(ctx, db.CreateSessionParams{
131+
ID: refreshPayload.ID,
132+
Username: user.Username,
133+
RefreshToken: refreshToken,
134+
UserAgent: ctx.Request.UserAgent(),
135+
ClientIp: ctx.ClientIP(),
136+
IsBlocked: false,
137+
ExpiresAt: refreshPayload.ExpiredAt,
138+
})
111139
if err != nil {
112140
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
113141
return
114142
}
115143

116144
resp := loginUserResponse{
117-
AccessToken: accessToken,
118-
User: newUserResponse(user),
145+
SessionID: session.ID,
146+
AccessToken: accessToken,
147+
AccessTokenExpiresAt: accessPayload.ExpiredAt,
148+
RefreshToken: refreshToken,
149+
RefreshTokenExpiresAt: refreshPayload.ExpiredAt,
150+
User: newUserResponse(user),
119151
}
120152

121153
ctx.JSON(http.StatusOK, resp)

app.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ DB_DRIVER=postgres
22
DB_SOURCE=postgresql://mars:mars@localhost:5555/tinybank?sslmode=disable
33
SERVER_ADDRESS=0.0.0.0:8080
44
TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012
5-
ACCESS_TOKEN_DURATION=15m
5+
ACCESS_TOKEN_DURATION=15m
6+
REFRESH_TOKEN_DURATION=24h
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE IF EXISTS "sessions";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE "sessions" (
2+
"id" uuid PRIMARY KEY,
3+
"username" varchar NOT NULL,
4+
"refresh_token" varchar NOT NULL,
5+
"user_agent" varchar NOT NULL,
6+
"client_ip" varchar NOT NULL,
7+
"is_blocked" boolean NOT NULL DEFAULT false,
8+
"expires_at" timestamptz NOT NULL,
9+
"created_at" timestamptz NOT NULL DEFAULT (now())
10+
);
11+
12+
ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");

db/mock/store.go

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

db/queries/session.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- name: CreateSession :one
2+
INSERT INTO sessions (
3+
id,
4+
username,
5+
refresh_token,
6+
user_agent,
7+
client_ip,
8+
is_blocked,
9+
expires_at
10+
) VALUES (
11+
$1, $2, $3, $4, $5, $6, $7
12+
) RETURNING *;
13+
14+
-- name: GetSession :one
15+
SELECT * FROM sessions
16+
WHERE id = $1 LIMIT 1;

0 commit comments

Comments
 (0)