Skip to content

Commit 7897b8b

Browse files
committed
- Middleware now returns 403 to avoid to many call's
- Validation for name and id params on endpoints - new struct sheared by signup and login - small code reading improvement - delete commented code - correct a panic on the code, returning proper error - rename a few functions
1 parent 62c6e42 commit 7897b8b

File tree

6 files changed

+120
-60
lines changed

6 files changed

+120
-60
lines changed

controllers/controller.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ const levenshtein = 0.8
2121
//Signup a new user to the database
2222
func Signup(c *gin.Context) {
2323
//get email/pass off req body
24-
var body struct {
25-
Email string
26-
Password string
27-
}
24+
var body models.InputBody
2825

2926
if c.Bind(&body) != nil {
3027
c.JSON(http.StatusBadRequest, gin.H{"Message": "Failed to read body"})
@@ -54,10 +51,7 @@ func Signup(c *gin.Context) {
5451
//Login verifies cookie session for login
5552
func Login(c *gin.Context) {
5653
// Get the email and password from request body
57-
var body struct {
58-
Email string
59-
Password string
60-
}
54+
var body models.InputBody
6155

6256
if c.Bind(&body) != nil {
6357
c.JSON(http.StatusBadRequest, gin.H{"Message": "Failed to read body"})
@@ -92,7 +86,7 @@ func Login(c *gin.Context) {
9286
c.SetCookie("token", token, 60*60, "/", "", false, true)
9387

9488
// Return success response
95-
c.JSON(http.StatusOK, gin.H{})
89+
c.JSON(http.StatusOK, gin.H{"Message": "Login successful"})
9690
}
9791

9892
//generateJWTToken generates a JWT token with a specified expiration time and user ID. It first sets the token expiration time based on the amountDays parameter passed into the function.
@@ -212,8 +206,10 @@ func SearchSimilarNames(c *gin.Context) {
212206
similarNames := findNames(metaphoneNames, name, levenshtein)
213207

214208
//for recall purposes we can't only search for metaphone exact match's if no similar word is found
209+
preloadTable := c.MustGet("nameTypes").([]models.NameType)
210+
215211
if len(metaphoneNames) == 0 || len(similarNames) == 0 {
216-
metaphoneNames = searchForAllSimilarMetaphone(nameMetaphone, c.MustGet("nameTypes").([]models.NameType))
212+
metaphoneNames = searchForAllSimilarMetaphone(nameMetaphone, preloadTable)
217213
similarNames = findNames(metaphoneNames, name, levenshtein)
218214

219215
if len(metaphoneNames) == 0 {
@@ -264,9 +260,6 @@ func SearchSimilarNames(c *gin.Context) {
264260

265261
//searchForAllSimilarMetaphone used in case of not finding exact metaphone match
266262
func searchForAllSimilarMetaphone(mtf string, names []models.NameType) []models.NameType {
267-
//var names []models.NameType
268-
//database.Db.Raw("SELECT * FROM name_types").Find(&names)
269-
270263
var rNames []models.NameType
271264
for _, n := range names {
272265
if Metaphone.IsMetaphoneSimilar(mtf, n.Metaphone) {

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ module github.com/Darklabel91/API_Names
33
go 1.18
44

55
require (
6+
github.com/Darklabel91/metaphone-br v0.0.0-20230327175255-f661f3ae637b
67
github.com/gin-gonic/gin v1.9.0
8+
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1
79
github.com/joho/godotenv v1.5.1
10+
golang.org/x/crypto v0.7.0
11+
golang.org/x/time v0.3.0
812
gorm.io/driver/mysql v1.4.7
913
gorm.io/gorm v1.24.6
1014
)
1115

1216
require (
1317
github.com/Darklabel91/Levenshtein v0.0.0-20230327182846-18e2b540c668 // indirect
14-
github.com/Darklabel91/metaphone-br v0.0.0-20230327175255-f661f3ae637b // indirect
1518
github.com/bytedance/sonic v1.8.5 // indirect
1619
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
1720
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -20,7 +23,6 @@ require (
2023
github.com/go-playground/validator/v10 v10.11.2 // indirect
2124
github.com/go-sql-driver/mysql v1.7.0 // indirect
2225
github.com/goccy/go-json v0.10.1 // indirect
23-
github.com/golang-jwt/jwt/v5 v5.0.0-rc.1 // indirect
2426
github.com/jinzhu/inflection v1.0.0 // indirect
2527
github.com/jinzhu/now v1.1.5 // indirect
2628
github.com/json-iterator/go v1.1.12 // indirect
@@ -33,7 +35,6 @@ require (
3335
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3436
github.com/ugorji/go/codec v1.2.11 // indirect
3537
golang.org/x/arch v0.3.0 // indirect
36-
golang.org/x/crypto v0.7.0 // indirect
3738
golang.org/x/net v0.8.0 // indirect
3839
golang.org/x/sys v0.6.0 // indirect
3940
golang.org/x/text v0.8.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/Darklabel91/Levenshtein v0.0.0-20230327180915-520182aba65a h1:/nTBmKXehgl606NX4oqbKXMC5fVWPyjNBiSfnX9S2jM=
2-
github.com/Darklabel91/Levenshtein v0.0.0-20230327180915-520182aba65a/go.mod h1:8sU0Aii5Eog/JhC/LtRaSR4jSvgQyDN84rG2ihtm1iU=
31
github.com/Darklabel91/Levenshtein v0.0.0-20230327182846-18e2b540c668 h1:F1VOn4ZXc8hBqtmNsSarRI/EwReb/TNDGGQt9tNWWDY=
42
github.com/Darklabel91/Levenshtein v0.0.0-20230327182846-18e2b540c668/go.mod h1:8sU0Aii5Eog/JhC/LtRaSR4jSvgQyDN84rG2ihtm1iU=
53
github.com/Darklabel91/metaphone-br v0.0.0-20230327175255-f661f3ae637b h1:ltrsS0rhJTqJqLHgULHSNSLBkht5tJ1tx7IJ12YRmXU=
@@ -90,6 +88,8 @@ golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
9088
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9189
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
9290
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
91+
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
92+
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
9393
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
9494
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
9595
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

middleware/requireAuth.go

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,108 @@ import (
44
"fmt"
55
"github.com/gin-gonic/gin"
66
"github.com/golang-jwt/jwt/v5"
7+
"golang.org/x/time/rate"
78
"net/http"
89
"os"
10+
"strconv"
11+
"strings"
912
"time"
1013
)
1114

12-
func RequireAuth(c *gin.Context) {
13-
//get the cookie off req
14-
tokenString := c.GetHeader("Token")
15+
const MaxThreadsByToken = 5
16+
17+
// RequireAuth returns a Gin middleware function that checks for a valid JWT token in the request header or cookie, and limits the rate of requests to prevent DDoS attacks.
18+
// - The rate limit is enforced using a token bucket algorithm.
19+
// - The rate limit and queue capacity can be adjusted by modifying the constants in the function.
20+
// - If the token is invalid or has expired, or if the request cannot be processed due to an error, the middleware function aborts the request with a 401 Unauthorized HTTP status code.
21+
func RequireAuth() gin.HandlerFunc {
22+
// Create a new rate limiter to limit the number of requests per second
23+
limiter := rate.NewLimiter(1000, MaxThreadsByToken)
24+
25+
return func(c *gin.Context) {
26+
// Check if the request has exceeded the rate limit
27+
if !limiter.Allow() {
28+
c.AbortWithStatus(http.StatusTooManyRequests)
29+
return
30+
}
31+
32+
// Get the token from the header or cookie
33+
tokenString := c.GetHeader("Token")
34+
var err error
35+
if tokenString == "" {
36+
tokenString, err = c.Cookie("token")
37+
if err != nil {
38+
c.AbortWithStatus(http.StatusUnauthorized)
39+
return
40+
}
41+
}
42+
43+
// Decode/validate the token
44+
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
45+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
46+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
47+
}
48+
49+
return []byte(os.Getenv("SECRET")), nil
50+
})
1551

16-
var err error
17-
if tokenString == "" {
18-
tokenString, err = c.Cookie("token")
1952
if err != nil {
2053
c.AbortWithStatus(http.StatusUnauthorized)
2154
return
2255
}
56+
57+
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
58+
// Check the expiration date
59+
if float64(time.Now().Unix()) > claims["exp"].(float64) {
60+
c.AbortWithStatus(http.StatusUnauthorized)
61+
return
62+
}
63+
64+
// Continue
65+
c.Next()
66+
} else {
67+
c.AbortWithStatus(http.StatusUnauthorized)
68+
return
69+
}
2370
}
71+
}
2472

25-
//decode/validate
26-
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
27-
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
28-
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
73+
// ValidateIDParam validates id param. It must contain only numbers
74+
func ValidateIDParam() gin.HandlerFunc {
75+
return func(c *gin.Context) {
76+
// Try to parse the ":id" parameter as an integer
77+
if _, err := strconv.Atoi(c.Param("id")); err != nil {
78+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
79+
"error": fmt.Sprintf("Invalid ':id' parameter: '%s' is not a valid integer", c.Param("id")),
80+
})
81+
return
2982
}
83+
c.Next()
84+
}
85+
}
3086

31-
return []byte(os.Getenv("SECRET")), nil
32-
})
87+
//ValidateNameParam validates :name param. It must not contain numbers or spaces
88+
func ValidateNameParam() gin.HandlerFunc {
89+
return func(c *gin.Context) {
90+
// Try to retrieve the ":name" parameter from the request context
91+
name := c.Param("name")
3392

34-
if err != nil {
35-
c.AbortWithStatus(http.StatusUnauthorized)
36-
return
37-
}
93+
// Check if the name contains whitespace
94+
if strings.Contains(name, " ") {
95+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
96+
"error": fmt.Sprintf("Invalid ':name' parameter: '%s' should contain a single word with no spaces", name),
97+
})
98+
return
99+
}
38100

39-
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
40-
//check the expiration date
41-
if float64(time.Now().Unix()) > claims["exp"].(float64) {
42-
c.AbortWithStatus(http.StatusUnauthorized)
101+
// Check if the name contains any numbers
102+
if _, err := strconv.Atoi(name); err == nil {
103+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
104+
"error": fmt.Sprintf("Invalid ':name' parameter: '%s' should not contain any numbers", name),
105+
})
43106
return
44107
}
45108

46-
//continue
47109
c.Next()
48-
} else {
49-
c.AbortWithStatus(http.StatusUnauthorized)
50-
return
51110
}
52-
53111
}

models/models.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ type MetaphoneR struct {
3838
Metaphone string `json:"Metaphone,omitempty"`
3939
NameVariations []string `json:"NameVariations,omitempty"`
4040
}
41+
42+
//InputBody struct for validation middleware
43+
type InputBody struct {
44+
Email string `json:"Email,omitempty"`
45+
Password string `json:"Password,omitempty"`
46+
}

routes/routes.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,30 @@ func HandleRequests() {
2323
return
2424
}
2525

26-
//signup and login
26+
//set up routes
2727
r.POST("/signup", controllers.Signup)
2828
r.POST("/login", controllers.Login)
2929

30-
//Other routes
31-
r.Use(middleware.RequireAuth)
30+
//define middleware that validate token
31+
r.Use(middleware.RequireAuth())
3232

33+
//set up caching middleware for GET requests
34+
r.GET("/:id", middleware.ValidateIDParam(), waitGroupID)
35+
r.DELETE("/:id", middleware.ValidateIDParam(), controllers.DeleteName)
36+
r.PATCH("/:id", middleware.ValidateIDParam(), controllers.UpdateName)
3337
r.POST("/name", controllers.CreateName)
34-
r.DELETE("/:id", controllers.DeleteName)
35-
r.PATCH("/:id", controllers.UpdateName)
36-
r.GET("/:id", WaitGroupID)
37-
r.GET("/name/:name", WaitGroupName)
38-
r.GET("/metaphone/:name", PreloadNameTypes(), WaitGroupMetaphone)
38+
r.GET("/name/:name", middleware.ValidateNameParam(), waitGroupName)
39+
r.GET("/metaphone/:name", middleware.ValidateNameParam(), preloadNameTypes(), middleware.ValidateNameParam(), waitGroupMetaphone)
3940

41+
// run
4042
err = r.Run(door)
4143
if err != nil {
42-
panic(err)
44+
return
4345
}
4446
}
4547

46-
//WaitGroupMetaphone crates a waiting group for handling requests using controllers.SearchSimilarNames
47-
func WaitGroupMetaphone(c *gin.Context) {
48+
//waitGroupMetaphone crates a waiting group for handling requests using controllers.SearchSimilarNames
49+
func waitGroupMetaphone(c *gin.Context) {
4850
var wg sync.WaitGroup
4951
wg.Add(1)
5052

@@ -57,8 +59,8 @@ func WaitGroupMetaphone(c *gin.Context) {
5759
wg.Wait()
5860
}
5961

60-
//WaitGroupName crates a waiting group for handling requests using controllers.GetName
61-
func WaitGroupName(c *gin.Context) {
62+
//waitGroupName crates a waiting group for handling requests using controllers.GetName
63+
func waitGroupName(c *gin.Context) {
6264
var wg sync.WaitGroup
6365
wg.Add(1)
6466

@@ -71,8 +73,8 @@ func WaitGroupName(c *gin.Context) {
7173
wg.Wait()
7274
}
7375

74-
// WaitGroupID crates a waiting group for handling requests using controllers.GetID
75-
func WaitGroupID(c *gin.Context) {
76+
// waitGroupID crates a waiting group for handling requests using controllers.GetID
77+
func waitGroupID(c *gin.Context) {
7678
var wg sync.WaitGroup
7779
wg.Add(1)
7880

@@ -85,8 +87,8 @@ func WaitGroupID(c *gin.Context) {
8587
wg.Wait()
8688
}
8789

88-
//PreloadNameTypes for better response time we load all records of the table
89-
func PreloadNameTypes() gin.HandlerFunc {
90+
//preloadNameTypes for better response time we load all records of the table
91+
func preloadNameTypes() gin.HandlerFunc {
9092
var nameTypes []models.NameType
9193
if err := database.Db.Find(&nameTypes).Error; err != nil {
9294
return nil

0 commit comments

Comments
 (0)