Skip to content

Commit bb43d12

Browse files
committed
add:jwt middleware
1 parent 8ab40c8 commit bb43d12

8 files changed

Lines changed: 215 additions & 11 deletions

File tree

part3/cmd/api/main.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"part3/internal/handler"
5+
"part3/internal/middleware"
56
"part3/internal/model"
67
"part3/internal/repository"
78
"part3/internal/service"
@@ -19,38 +20,45 @@ func main() {
1920
}
2021

2122
// Migrate the schema
22-
db.AutoMigrate(&model.Task{}, &model.Schedule{})
23+
db.AutoMigrate(&model.Task{}, &model.Schedule{}, &model.User{})
2324

2425
// Initialize repositories
2526
taskRepo := repository.NewTaskRepository(db)
2627
scheduleRepo := repository.NewScheduleRepository(db)
27-
2828
// Initialize services
2929
taskService := service.NewTaskService(taskRepo)
3030
scheduleService := service.NewScheduleService(scheduleRepo, taskRepo)
31+
authService := service.NewAuthService(db)
3132

3233
// Initialize handlers
3334
taskHandler := handler.NewTaskHandler(taskService)
3435
scheduleHandler := handler.NewScheduleHandler(scheduleService)
36+
authHandler := handler.NewAuthHandler(authService)
3537

3638
// Set up Gin router
3739
r := gin.Default()
3840

41+
// Auth routes
42+
r.POST("/register", authHandler.Register)
43+
r.POST("/login", authHandler.Login)
44+
3945
// Task routes
46+
authGroup := r.Group("/schedules")
47+
authGroup.Use(middleware.AuthMiddleware())
48+
{
49+
authGroup.POST("/", scheduleHandler.CreateSchedule)
50+
authGroup.GET("/:id", scheduleHandler.GetSchedule)
51+
authGroup.GET("/tasks/:taskId/schedules", scheduleHandler.GetSchedulesByTask)
52+
authGroup.PUT("/:id", scheduleHandler.UpdateSchedule)
53+
authGroup.DELETE("/:id", scheduleHandler.DeleteSchedule)
54+
authGroup.GET("/", scheduleHandler.ListSchedules)
55+
}
4056
r.POST("/tasks", taskHandler.CreateTask)
4157
r.GET("/tasks/:id", taskHandler.GetTask)
4258
r.PUT("/tasks/:id", taskHandler.UpdateTask)
4359
r.DELETE("/tasks/:id", taskHandler.DeleteTask)
4460
r.GET("/tasks", taskHandler.ListTasks)
4561

46-
// Schedule routes
47-
r.POST("/schedules", scheduleHandler.CreateSchedule)
48-
r.GET("/schedules/:id", scheduleHandler.GetSchedule)
49-
r.GET("/tasks/:taskId/schedules", scheduleHandler.GetSchedulesByTask)
50-
r.PUT("/schedules/:id", scheduleHandler.UpdateSchedule)
51-
r.DELETE("/schedules/:id", scheduleHandler.DeleteSchedule)
52-
r.GET("/schedules", scheduleHandler.ListSchedules)
53-
5462
// Start the server
5563
r.Run(":8080")
5664
}

part3/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ go 1.24.4
44

55
require (
66
github.com/gin-gonic/gin v1.11.0
7+
github.com/golang-jwt/jwt/v5 v5.3.0
78
github.com/stretchr/testify v1.11.1
89
go.uber.org/mock v0.6.0
10+
golang.org/x/crypto v0.41.0
911
gorm.io/driver/sqlite v1.6.0
1012
gorm.io/gorm v1.31.1
1113
)
@@ -38,7 +40,6 @@ require (
3840
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3941
github.com/ugorji/go/codec v1.3.0 // indirect
4042
golang.org/x/arch v0.20.0 // indirect
41-
golang.org/x/crypto v0.41.0 // indirect
4243
golang.org/x/mod v0.27.0 // indirect
4344
golang.org/x/net v0.43.0 // indirect
4445
golang.org/x/sync v0.16.0 // indirect

part3/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
2525
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
2626
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
2727
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
28+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
29+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
2830
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2931
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3032
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

part3/internal/handler/auth.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package handler
2+
3+
import (
4+
"net/http"
5+
"part3/internal/service"
6+
7+
"github.com/gin-gonic/gin"
8+
)
9+
10+
type AuthHandler struct {
11+
service service.AuthService
12+
}
13+
14+
func NewAuthHandler(service service.AuthService) *AuthHandler {
15+
return &AuthHandler{service: service}
16+
}
17+
18+
type AuthRequest struct {
19+
Username string `json:"username" binding:"required"`
20+
Password string `json:"password" binding:"required"`
21+
}
22+
23+
func (h *AuthHandler) Register(c *gin.Context) {
24+
var req AuthRequest
25+
if err := c.ShouldBindJSON(&req); err != nil {
26+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
27+
return
28+
}
29+
30+
if err := h.service.Register(req.Username, req.Password); err != nil {
31+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register user"})
32+
return
33+
}
34+
35+
c.Status(http.StatusCreated)
36+
}
37+
38+
func (h *AuthHandler) Login(c *gin.Context) {
39+
var req AuthRequest
40+
if err := c.ShouldBindJSON(&req); err != nil {
41+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
42+
return
43+
}
44+
45+
token, err := h.service.Login(req.Username, req.Password)
46+
if err != nil {
47+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
48+
return
49+
}
50+
51+
c.JSON(http.StatusOK, gin.H{"token": token})
52+
}

part3/internal/middleware/auth.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package middleware
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/gin-gonic/gin"
9+
"github.com/golang-jwt/jwt/v5"
10+
)
11+
12+
var jwtSecretKey = []byte("your_secret_key") // Serviceと同じキーを使う
13+
14+
func AuthMiddleware() gin.HandlerFunc {
15+
return func(c *gin.Context) {
16+
authHeader := c.GetHeader("Authorization")
17+
if authHeader == "" {
18+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
19+
return
20+
}
21+
22+
// "Bearer <token>" 形式からトークン部分のみ抽出
23+
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
24+
if tokenString == authHeader {
25+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Bearer token format is required"})
26+
return
27+
}
28+
29+
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
30+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
31+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
32+
}
33+
return jwtSecretKey, nil
34+
})
35+
36+
if err != nil || !token.Valid {
37+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
38+
return
39+
}
40+
41+
// トークンからユーザーIDを取り出し、コンテキストにセット(後続のハンドラで利用可能)
42+
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
43+
if sub, ok := claims["sub"].(float64); ok {
44+
c.Set("userID", uint(sub))
45+
}
46+
}
47+
48+
c.Next()
49+
}
50+
}

part3/internal/model/user.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package model
2+
3+
import (
4+
"time"
5+
6+
"gorm.io/gorm"
7+
)
8+
9+
type User struct {
10+
ID uint `gorm:"primaryKey" json:"id"`
11+
Username string `gorm:"unique;not null" json:"username"`
12+
Password string `gorm:"not null" json:"-"` // JSONには含めない
13+
CreatedAt time.Time `json:"created_at"`
14+
UpdatedAt time.Time `json:"updated_at"`
15+
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
16+
}

part3/internal/service/auth.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package service
2+
3+
import (
4+
"errors"
5+
"time"
6+
7+
"part3/internal/model"
8+
9+
"github.com/golang-jwt/jwt/v5"
10+
"golang.org/x/crypto/bcrypt"
11+
"gorm.io/gorm"
12+
)
13+
14+
// 署名に使用する秘密鍵(本番環境では環境変数から読み込むべきです)
15+
var jwtSecretKey = []byte("your_secret_key")
16+
17+
type AuthService interface {
18+
Login(username, password string) (string, error)
19+
Register(username, password string) error
20+
}
21+
22+
type authService struct {
23+
db *gorm.DB
24+
}
25+
26+
func NewAuthService(db *gorm.DB) AuthService {
27+
return &authService{db: db}
28+
}
29+
30+
func (s *authService) Register(username, password string) error {
31+
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
32+
if err != nil {
33+
return err
34+
}
35+
36+
user := model.User{
37+
Username: username,
38+
Password: string(hashedPassword),
39+
}
40+
41+
return s.db.Create(&user).Error
42+
}
43+
44+
func (s *authService) Login(username, password string) (string, error) {
45+
var user model.User
46+
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
47+
return "", errors.New("invalid credentials")
48+
}
49+
50+
// パスワードの検証
51+
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
52+
return "", errors.New("invalid credentials")
53+
}
54+
55+
// JWTトークンの生成
56+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
57+
"sub": user.ID, // Subject (ユーザーID)
58+
"exp": time.Now().Add(time.Hour * 24).Unix(), // 有効期限 (24時間)
59+
})
60+
61+
tokenString, err := token.SignedString(jwtSecretKey)
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
return tokenString, nil
67+
}

presentation-part3.typ

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ authGroup.Use(middleware.AuthMiddleware())
176176
}
177177
```
178178

179+
== ユーザー登録とログイン
180+
- internal/handler/auth.go
181+
```bash
182+
curl -X POST http://localhost:8080/login \
183+
-H "Content-Type: application/json" \
184+
-d '{"username":"user1","password":"pass123"}'
185+
```
186+
179187
= 4. DB永続化(Docker Compose)
180188
== PostgreSQLの導入
181189
- Docker ComposeでPostgreSQLコンテナを起動

0 commit comments

Comments
 (0)