Skip to content

Commit adff643

Browse files
committed
add: jwt for api access control
1 parent ed4896e commit adff643

File tree

33 files changed

+588
-178
lines changed

33 files changed

+588
-178
lines changed

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Golang CRUD Service
22

3-
This is a sample CRUD service built with Golang, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database and **Redis** for caching and other in-memory operations.
3+
This is a sample CRUD service built with Golang, demonstrating a layered architecture that separates concerns into handler, service, repository, and model layers. The project uses **TiDB** as the relational database, **Redis** for caching and other in-memory operations, and **JWT** for authentication.
44

55
## 🚀 Project Purpose
66

@@ -15,6 +15,7 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
1515
- **[Redis](https://redis.io)** — In-memory data store
1616
- **[Validator](https://github.com/go-playground/validator)** — Input validation
1717
- **[Copier](https://github.com/jinzhu/copier)** — Object copying for DTOs
18+
- **[JWT](https://github.com/golang-jwt/jwt)** — JSON Web Token implementation for authentication
1819

1920
## 📁 Project Structure
2021

@@ -23,27 +24,29 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
2324
│ └── app/
2425
│ └── main.go # Application entry point
2526
├── pkg/ # Shared package configure
26-
│ └── bcrypt/
27-
| └── bcrypt.go # Bcrypt hashing
28-
│ └── redis/
29-
| └── redis.go # Redis configuration
30-
│ └── db/
31-
| └── db.go # DB configuration
27+
│ └── crypto/
28+
│ └── aes.go # Advanced Encryption Standard encryption
29+
│ └── bcrypt.go # Bcrypt hashing
30+
│ └── redis/
31+
│ └── redis.go # Redis configuration
32+
│ └── db/
33+
│ └── db.go # DB configuration
3234
├── api/
3335
│ └── student/
3436
│ ├── route.go # Student-related endpoints
3537
│ └── handler.go # Logic for handling student API requests
3638
├── internal/
37-
│ ├── service/ # Business logic, called from handler
39+
│ ├── middleware/ # HTTP middleware (e.g., auth, logging)
3840
│ ├── repository/ # Data access layer (calls GORM & queries DB)
41+
│ ├── service/ # Business logic, called from handler
3942
│ ├── model/ # Database models/entities
4043
│ └── dto/ # DTOs for transforming request/response data
4144
```
4245

4346
## 📌 Key Concepts
4447

4548
- **Layered Architecture**: Divides the project into clear layers for maintainability and scalability.
46-
- **DTO Pattern**: Uses `copier` to map between internal models and response/request structures.
49+
- **DTO Pattern**: Uses `copier` to map between internal models and request/response structures.
4750
- **Validation**: Ensures request payloads are validated with `go-playground/validator`.
4851
- **ORM**: Leverages GORM to interact with TiDB in a concise and type-safe way.
4952

@@ -63,10 +66,9 @@ Build a sample CRUD service API with Golang to showcase clean code structure and
6366
## 🔧 Future Enhancements
6467

6568
- Add unit tests and integration tests
66-
- Implement authentication and authorization
6769
- Dockerize the application
6870
- Add Swagger/OpenAPI documentation
6971

7072
## 📄 License
7173

72-
This project is open-source and available under the [MIT License](LICENSE).
74+
This project is open-source and available under the [MIT License]().

api/auth/handler.go

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,90 @@
11
package auth
22

33
import (
4-
"learn-fiber/api/response"
5-
"learn-fiber/config"
6-
"learn-fiber/internal/util/common"
7-
"time"
8-
94
"github.com/gofiber/fiber/v2"
10-
"github.com/golang-jwt/jwt/v5"
5+
"github.com/jinzhu/copier"
116
"gorm.io/gorm"
7+
"learn-fiber/api/response"
8+
"learn-fiber/internal/constant"
9+
"learn-fiber/internal/dto"
10+
"learn-fiber/internal/ierror"
11+
"learn-fiber/internal/service"
12+
"learn-fiber/internal/util/common"
13+
"learn-fiber/internal/util/validator"
1214
)
1315

1416
type Handler struct {
15-
db *gorm.DB
17+
svc service.Auth
1618
}
1719

1820
func NewHandler(db *gorm.DB) *Handler {
19-
return &Handler{db: db}
21+
svc := service.NewAuth(db)
22+
return &Handler{svc: svc}
2023
}
2124

2225
func (h *Handler) Login(c *fiber.Ctx) error {
23-
req, err := common.GetRequestBody[Request](c)
26+
req, _ := common.GetRequestBody[dto.LoginRequest](c)
27+
if hasError, err := validator.V.Valid(req); hasError {
28+
return ierror.NewValidationError(err)
29+
}
30+
result, err := h.svc.Login(req)
31+
if err != nil {
32+
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
33+
}
34+
var res TokenResponse
35+
if err = copier.Copy(&res, &result); err != nil {
36+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
37+
}
38+
return response.SendSuccessResponse(c, res)
39+
}
40+
41+
func (h *Handler) Logout(c *fiber.Ctx) error {
42+
token, err := common.GetAccessToken(c)
43+
if err != nil {
44+
return ierror.NewAuthenticationError(ierror.ErrCodeAuthenticationError, err.Error())
45+
}
46+
if err := h.svc.Logout(token); err != nil {
47+
return ierror.NewServerError(ierror.ErrCodeTokenError, err.Error())
48+
}
49+
return response.SendSuccessResponse(c, constant.Success)
50+
}
51+
52+
func (h *Handler) RenewToken(c *fiber.Ctx) error {
53+
req, _ := common.GetRequestBody[dto.RenewTokenRequest](c)
54+
if hasError, err := validator.V.Valid(req); hasError {
55+
return ierror.NewValidationError(err)
56+
}
57+
result, err := h.svc.RenewToken(req)
2458
if err != nil {
25-
return err
59+
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
60+
}
61+
var res TokenResponse
62+
if err = copier.Copy(&res, &result); err != nil {
63+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
2664
}
65+
return response.SendSuccessResponse(c, res)
2766

28-
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
29-
"username": req.Username,
30-
"role": "admin",
31-
"exp": time.Now().Add(time.Hour * 1).Unix(),
32-
})
67+
}
3368

34-
secret := []byte(config.Cfg.JWT.Secret)
35-
res, err := token.SignedString(secret)
69+
func (h *Handler) Info(c *fiber.Ctx) error {
70+
result, err := h.svc.Info(common.GetUser(c))
3671
if err != nil {
37-
return c.Status(500).JSON(fiber.Map{"error": "Could not generate token"})
72+
return ierror.NewClientError(200, ierror.ErrCodeAuthenticationError, err.Error())
73+
}
74+
var res UserResponse
75+
if err = copier.Copy(&res, &result); err != nil {
76+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
3877
}
3978
return response.SendSuccessResponse(c, res)
4079
}
80+
81+
func (h *Handler) ChangePassword(c *fiber.Ctx) error {
82+
req, _ := common.GetRequestBody[dto.ChangePassRequest](c)
83+
if hasError, err := validator.V.Valid(req); hasError {
84+
return ierror.NewValidationError(err)
85+
}
86+
if err := h.svc.ChangePassword(common.GetUser(c), req); err != nil {
87+
return ierror.NewClientError(200, ierror.ErrCodeValidationError, err.Error())
88+
}
89+
return response.SendSuccessResponse(c, constant.Success)
90+
}

api/auth/request.go

Lines changed: 0 additions & 6 deletions
This file was deleted.

api/auth/response.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package auth
2+
3+
type TokenResponse struct {
4+
Expired int64 `json:"expired"`
5+
AccessToken string `json:"accessToken"`
6+
RefreshToken string `json:"refreshToken"`
7+
}
8+
9+
type UserResponse struct {
10+
Id uint `json:"id"`
11+
Username string `json:"username"`
12+
Role string `json:"role"`
13+
}

api/auth/routes.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ package auth
33
import (
44
"github.com/gofiber/fiber/v2"
55
"gorm.io/gorm"
6+
"learn-fiber/internal/middleware"
67
)
78

89
func AddRoutes(r fiber.Router, db *gorm.DB) {
910
handler := NewHandler(db)
1011
router := r.Group("/auth")
1112
router.Post("login", handler.Login)
13+
router.Post("renew-token", handler.RenewToken)
14+
router.Get("info", middleware.JWTProtected(), handler.Info)
15+
router.Post("logout", middleware.JWTProtected(), handler.Logout)
16+
router.Post("change-password", middleware.JWTProtected(), handler.ChangePassword)
1217
}

api/department/handler.go

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package department
22

33
import (
4+
"errors"
45
"learn-fiber/api/response"
6+
"learn-fiber/internal/constant"
57
"learn-fiber/internal/dto"
68
"learn-fiber/internal/ierror"
79
"learn-fiber/internal/service"
@@ -14,36 +16,43 @@ import (
1416
)
1517

1618
type Handler struct {
17-
service service.Department
19+
svc service.Department
1820
}
1921

2022
func NewHandler(db *gorm.DB) *Handler {
2123
svc := service.NewDepartment(db)
22-
return &Handler{service: svc}
24+
return &Handler{svc: svc}
2325
}
2426

2527
func (h *Handler) GetList(c *fiber.Ctx) error {
2628
query, _ := common.GetQueryParam[dto.QueryParams](c)
2729
if hasError, err := validator.V.Valid(query); hasError {
2830
return ierror.NewValidationError(err)
2931
}
30-
result, page, err := h.service.GetAllDepartment(query)
32+
result, page, err := h.svc.GetAllDepartment(query)
3133
if err != nil {
32-
return response.SendFailResponse(c, err.Error())
34+
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
3335
}
3436
var res []Response
35-
_ = copier.Copy(&res, &result)
37+
if err = copier.Copy(&res, &result); err != nil {
38+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
39+
}
3640
return response.SendSuccessResponsePaging(c, res, page)
3741
}
3842

3943
func (h *Handler) GetById(c *fiber.Ctx) error {
4044
id := c.Params("id")
41-
result, err := h.service.GetDepartment(id)
45+
result, err := h.svc.GetDepartment(id)
4246
if err != nil {
43-
return response.SendFailResponse(c, err.Error())
47+
if errors.Is(err, gorm.ErrRecordNotFound) {
48+
return response.SendSuccessResponse(c, err.Error())
49+
}
50+
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
4451
}
4552
var res Response
46-
_ = copier.Copy(&res, &result)
53+
if err = copier.Copy(&res, &result); err != nil {
54+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
55+
}
4756
return response.SendSuccessResponse(c, res)
4857
}
4958

@@ -52,12 +61,14 @@ func (h *Handler) Create(c *fiber.Ctx) error {
5261
if hasError, err := validator.V.Valid(req); hasError {
5362
return ierror.NewValidationError(err)
5463
}
55-
result, err := h.service.CreateDepartment(req)
64+
result, err := h.svc.CreateDepartment(req)
5665
if err != nil {
57-
return response.SendFailResponse(c, err.Error())
66+
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
5867
}
5968
var res Response
60-
_ = copier.Copy(&res, &result)
69+
if err = copier.Copy(&res, &result); err != nil {
70+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
71+
}
6172
return response.SendSuccessResponse(c, res)
6273
}
6374

@@ -66,17 +77,24 @@ func (h *Handler) Update(c *fiber.Ctx) error {
6677
if hasError, err := validator.V.Valid(req); hasError {
6778
return ierror.NewValidationError(err)
6879
}
69-
result, err := h.service.UpdateDepartment(req)
80+
result, err := h.svc.UpdateDepartment(req)
7081
if err != nil {
71-
return response.SendFailResponse(c, err.Error())
82+
if errors.Is(err, gorm.ErrRecordNotFound) {
83+
return ierror.NewClientError(200, ierror.ErrCodeDataNotFound, err.Error())
84+
}
85+
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
7286
}
7387
var res Response
74-
_ = copier.Copy(&res, &result)
88+
if err = copier.Copy(&res, &result); err != nil {
89+
return ierror.NewServerError(ierror.ErrCodeDtoError, err.Error())
90+
}
7591
return response.SendSuccessResponse(c, res)
7692
}
7793

7894
func (h *Handler) Delete(c *fiber.Ctx) error {
7995
id := c.Params("id")
80-
_ = h.service.DeleteDepartment(id)
81-
return response.SendSuccessResponse(c, nil)
96+
if err := h.svc.DeleteDepartment(id); err != nil {
97+
return ierror.NewServerError(ierror.ErrCodeDatabaseError, err.Error())
98+
}
99+
return response.SendSuccessResponse(c, constant.Success)
82100
}

api/response/success.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
type SuccessResponse struct {
1212
Message string `json:"message,omitempty"`
1313
Paging *pagination.PageResponse `json:"pagination,omitempty"`
14-
Data interface{} `json:"data,omitempty"`
14+
Data interface{} `json:"data"`
1515
}
1616

1717
func SendSuccessResponse(c *fiber.Ctx, data interface{}) error {

0 commit comments

Comments
 (0)