From 6c77e79357d64a42bb35c03b1d451a180e70f2fe Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:18:44 +0000 Subject: [PATCH 1/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go new file mode 100644 index 00000000..f44d1a55 --- /dev/null +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -0,0 +1,363 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "golang.org/x/time/rate" +) + +type Article struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Author string `json:"author"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +type APIResponse struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + RequestID string `json:"request_id,omitempty"` +} + +var articles = []Article{ + {ID: 1, Title: "Getting Started with Go", Content: "Go is a programming language", Author: "John Doe", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {ID: 2, Title: "Web Development with Gin", Content: "Gin is a web framework", Author: "Jane Smith", CreatedAt: time.Now(), UpdatedAt: time.Now()}, +} +var nextID = 3 +var ( + rateLimiters = make(map[string]*rate.Limiter) + rateLimitMutex sync.Mutex +) + +func main() { + router := gin.New() + router.Use( + RequestIDMiddleware(), + ErrorHandlerMiddleware(), + LoggingMiddleware(), + CORSMiddleware(), + RateLimitMiddleware(), + ContentTypeMiddleware(), + ) + + public := router.Group("/") + { + public.GET("/ping", ping) + public.GET("/articles/:id", getArticle) + public.GET("/articles", getArticles) + } + + private := router.Group("/").Use(AuthMiddleware()) + { + private.POST("/articles", createArticle) + private.PUT("/articles/:id", updateArticle) + private.DELETE("/articles/:id", deleteArticle) + private.GET("/admin/stats", getStats) + } + + router.Run(":8080") + +} +func RequestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + } + c.Set("request_id", requestID) + c.Writer.Header().Set("X-Request-ID", requestID) + c.Next() + } +} +func LoggingMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + c.Next() + duration := time.Since(start) + entry := map[string]interface{}{ + "request_id": c.GetString("request_id"), + "method": c.Request.Method, + "path": c.Request.URL.Path, + "status": c.Writer.Status(), + "duration": duration.Milliseconds(), + "ip": c.ClientIP(), + "user_agent": c.Request.UserAgent(), + } + if c.Writer.Status() >= 400 { + log.Printf("ERROR: %+v", entry) + } else { + log.Printf("INFO: %+v", entry) + } + } +} + +func getUserRole(apiKey string) (bool, string) { + roles := map[string]string{ + "admin-key-123": "admin", + "user-key-456": "user"} + val, prs := roles[apiKey] + if prs { + return true, val + } + return false, "" +} +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + apiKey := c.GetHeader("X-API-Key") + if apiKey == "" { + c.JSON(401, APIResponse{Success: false, Error: "API key required"}) + c.Abort() + return + } + is_valid, user_role := getUserRole(apiKey) + if !is_valid { + c.JSON(401, APIResponse{Success: false, Error: "Invalid API key"}) + c.Abort() + return + } + c.Set("user_role", user_role) + c.Next() + } +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + // TODO these are origins beside myself??? + allowedOrigins := map[string]bool{ + "http://localhost:3000": true, + "https://myapp.com": true, + } + if allowedOrigins[origin] { + c.Header("Access-Control-Allow-Origin", origin) + } + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key, X-Request-ID") + c.Header("Access-Control-Allow-Credentials", "true") + if c.Request.Method == "OPTIONS" { + c.JSON(204, APIResponse{Success: false, Error: "Forbidden"}) + c.Abort() + return + } + c.Next() + } +} + +func RateLimitMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + rateLimitMutex.Lock() + limiter, ok := rateLimiters[ip] + if !ok { + limiter = rate.NewLimiter(rate.Every(time.Minute/100), 100) + rateLimiters[ip] = limiter + } + rateLimitMutex.Unlock() + + c.Writer.Header().Set("X-RateLimit-Limit", "100") + c.Writer.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) + + if !limiter.Allow() { + c.Writer.Header().Set("X-RateLimit-Remaining", "0") + errResponse(c, http.StatusTooManyRequests, "Rate limit exceeded") + c.Abort() + return + } + + remaining := int(limiter.Tokens()) + c.Writer.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remaining)) + c.Next() + } +} + +func ContentTypeMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Method == "POST" || c.Request.Method == "PUT" { + contentType := c.GetHeader("Content-Type") + if !strings.HasPrefix(contentType, "application/json") { + c.JSON(415, APIResponse{Success: false, Error: "Content-Type must be application/json"}) + c.Abort() + return + } + } + c.Next() + } +} + + +func ErrorHandlerMiddleware() gin.HandlerFunc { + return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + c.JSON(http.StatusInternalServerError, APIResponse{ + Success: false, + Error: "Internal server error", + Message: fmt.Sprintf("%v", recovered), + RequestID: c.GetString("request_id"), + }) + c.Abort() + }) +} + +func ping(c *gin.Context) { + c.JSON(200, APIResponse{Success: true, RequestID: c.GetString("request_id")}) + +} + +func getArticles(c *gin.Context) { + c.JSON(200, APIResponse{ + Success: true, + Data: articles, + Message: "all articles", + RequestID: c.GetString("request_id")}) +} + +func getArticle(c *gin.Context) { + id := c.Param("id") + articleID, err := strconv.Atoi(id) + if err != nil { + c.JSON(400, APIResponse{Success: false, Error: "Invalid ID", RequestID: c.GetString("request_id")}) + return + } + article, ind := findArticleByID(articleID) + if ind != -1 { + c.JSON(200, APIResponse{ + Success: true, + Data: article, + Message: "article retrieved successfully", + RequestID: c.GetString("request_id")}) + + } else { + c.JSON(404, APIResponse{ + Success: false, + Error: "article not found", + RequestID: c.GetString("request_id"), + }) + + } + +} + +func createArticle(c *gin.Context) { + var newArticle Article + if err := c.ShouldBindJSON(&newArticle); err != nil { + c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + return + } + nextID++ + newArticle.ID = nextID + if err := validateArticle(newArticle); err != nil { + c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + return + } + articles = append(articles, newArticle) + c.JSON(201, APIResponse{Success: true, Data: newArticle, Message: "Article created"}) + +} + +func updateArticle(c *gin.Context) { + id := c.Param("id") + articleID, err := strconv.Atoi(id) + if err != nil { + c.JSON(400, APIResponse{Success: false, Error: "Invalid ID"}) + return + } + var newArticle Article + if err := c.ShouldBindJSON(&newArticle); err != nil { + c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + return + } + + if err := validateArticle(newArticle); err != nil { + c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + return + } + + article, ind := findArticleByID(articleID) + if ind != -1 { + article.ID = newArticle.ID + article.Author = newArticle.Author + article.Content = newArticle.Content + article.Title = newArticle.Title + article.UpdatedAt = time.Now() + c.JSON(200, APIResponse{ + Success: true, + Data: article, + Message: "Users updated successfully"}) + } else { + c.JSON(404, APIResponse{Success: false, Error: "article not found"}) + } +} + +func deleteArticle(c *gin.Context) { + id := c.Param("id") + articleID, err := strconv.Atoi(id) + if err != nil { + c.JSON(400, APIResponse{Success: false, Error: "Invalid ID"}) + return + } + _, ind := findArticleByID(articleID) + if ind != -1 { + articles[ind] = articles[len(articles)-1] + articles = articles[:len(articles)-1] + c.JSON(200, APIResponse{Success: true, Message: "article deleted successfully"}) + } else { + c.JSON(404, APIResponse{Success: false, Error: "article not found"}) + } +} + +// getStats handles GET /admin/stats - get API usage statistics (admin only) +func getStats(c *gin.Context) { + if c.GetString("user_role") != "admin" { + c.JSON(403, APIResponse{Success: false, Error: "Unauthorized"}) + return + } + stats := map[string]interface{}{ + "total_articles": len(articles), + "total_requests": 10, + "uptime": "24h", + } + c.JSON(200, APIResponse{Success: true, Data: stats, Message: "stats"}) +} + +func findArticleByID(id int) (*Article, int) { + for ind, article := range articles { + if article.ID == id { + return &article, ind + } + } + return nil, -1 +} + +func validateArticle(article Article) error { + if article.Title == "" || article.Content == "" || article.Author == "" { + return errors.New("name is required") + } + return nil +} + +func okResponse(c *gin.Context, status int, message string, data interface{}) { + c.JSON(status, APIResponse{ + Success: true, + Data: data, + Message: message, + RequestID: c.GetString("request_id"), + }) +} +func errResponse(c *gin.Context, status int, msg string) { + c.JSON(status, APIResponse{ + Success: false, + Error: msg, + RequestID: c.GetString("request_id"), + }) +} From 283efef47ce17ec8db74f5eb51915be81b3d4d8a Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:03:52 +0000 Subject: [PATCH 2/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index f44d1a55..032c2a04 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -35,8 +35,9 @@ var articles = []Article{ {ID: 1, Title: "Getting Started with Go", Content: "Go is a programming language", Author: "John Doe", CreatedAt: time.Now(), UpdatedAt: time.Now()}, {ID: 2, Title: "Web Development with Gin", Content: "Gin is a web framework", Author: "Jane Smith", CreatedAt: time.Now(), UpdatedAt: time.Now()}, } -var nextID = 3 var ( + nextID = 3 + articlesMutex sync.RWMutex rateLimiters = make(map[string]*rate.Limiter) rateLimitMutex sync.Mutex ) @@ -66,8 +67,9 @@ func main() { private.DELETE("/articles/:id", deleteArticle) private.GET("/admin/stats", getStats) } - - router.Run(":8080") + if err := router.Run(":8080"); err != nil { + log.Fatalf("Failed to start server: %v", err) + } } func RequestIDMiddleware() gin.HandlerFunc { @@ -118,16 +120,16 @@ func AuthMiddleware() gin.HandlerFunc { apiKey := c.GetHeader("X-API-Key") if apiKey == "" { c.JSON(401, APIResponse{Success: false, Error: "API key required"}) - c.Abort() + c.Abort() return } - is_valid, user_role := getUserRole(apiKey) - if !is_valid { + isValid, userRole := getUserRole(apiKey) + if !isValid { c.JSON(401, APIResponse{Success: false, Error: "Invalid API key"}) c.Abort() return } - c.Set("user_role", user_role) + c.Set("user_role", userRole) c.Next() } } @@ -135,7 +137,6 @@ func AuthMiddleware() gin.HandlerFunc { func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { origin := c.Request.Header.Get("Origin") - // TODO these are origins beside myself??? allowedOrigins := map[string]bool{ "http://localhost:3000": true, "https://myapp.com": true, @@ -147,8 +148,7 @@ func CORSMiddleware() gin.HandlerFunc { c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key, X-Request-ID") c.Header("Access-Control-Allow-Credentials", "true") if c.Request.Method == "OPTIONS" { - c.JSON(204, APIResponse{Success: false, Error: "Forbidden"}) - c.Abort() + c.AbortWithStatus(http.StatusNoContent) return } c.Next() @@ -165,17 +165,14 @@ func RateLimitMiddleware() gin.HandlerFunc { rateLimiters[ip] = limiter } rateLimitMutex.Unlock() - c.Writer.Header().Set("X-RateLimit-Limit", "100") c.Writer.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) - if !limiter.Allow() { c.Writer.Header().Set("X-RateLimit-Remaining", "0") - errResponse(c, http.StatusTooManyRequests, "Rate limit exceeded") + c.JSON(http.StatusTooManyRequests, APIResponse{Success: false, Error: "rate limit exceeded"}) c.Abort() return } - remaining := int(limiter.Tokens()) c.Writer.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remaining)) c.Next() @@ -195,10 +192,10 @@ func ContentTypeMiddleware() gin.HandlerFunc { c.Next() } } - - +// the assignment required to return error message, remove Message in production and use Internal Error instead func ErrorHandlerMiddleware() gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + log.Printf("Panic recovered: %v", recovered) c.JSON(http.StatusInternalServerError, APIResponse{ Success: false, Error: "Internal server error", @@ -215,6 +212,10 @@ func ping(c *gin.Context) { } func getArticles(c *gin.Context) { + articlesMutex.RLock() + articlesTemp := make([]Article, len(articles)) + copy(articlesTemp, articles) + articlesMutex.RUnlock() c.JSON(200, APIResponse{ Success: true, Data: articles, @@ -229,7 +230,9 @@ func getArticle(c *gin.Context) { c.JSON(400, APIResponse{Success: false, Error: "Invalid ID", RequestID: c.GetString("request_id")}) return } + articlesMutex.RLock() article, ind := findArticleByID(articleID) + articlesMutex.RUnlock() if ind != -1 { c.JSON(200, APIResponse{ Success: true, @@ -254,13 +257,18 @@ func createArticle(c *gin.Context) { c.JSON(400, APIResponse{Success: false, Error: err.Error()}) return } - nextID++ - newArticle.ID = nextID + if err := validateArticle(newArticle); err != nil { c.JSON(400, APIResponse{Success: false, Error: err.Error()}) return } + articlesMutex.Lock() + nextID++ + newArticle.ID = nextID + newArticle.CreatedAt = time.Now() + newArticle.UpdatedAt = time.Now() articles = append(articles, newArticle) + articlesMutex.Unlock() c.JSON(201, APIResponse{Success: true, Data: newArticle, Message: "Article created"}) } @@ -282,23 +290,24 @@ func updateArticle(c *gin.Context) { c.JSON(400, APIResponse{Success: false, Error: err.Error()}) return } - + articlesMutex.Lock() article, ind := findArticleByID(articleID) if ind != -1 { - article.ID = newArticle.ID article.Author = newArticle.Author article.Content = newArticle.Content article.Title = newArticle.Title article.UpdatedAt = time.Now() + articles[ind] = *article // Persist back to slice + articlesMutex.Unlock() c.JSON(200, APIResponse{ Success: true, Data: article, - Message: "Users updated successfully"}) + Message: "Article updated successfully"}) } else { + articlesMutex.Unlock() c.JSON(404, APIResponse{Success: false, Error: "article not found"}) } } - func deleteArticle(c *gin.Context) { id := c.Param("id") articleID, err := strconv.Atoi(id) @@ -306,12 +315,15 @@ func deleteArticle(c *gin.Context) { c.JSON(400, APIResponse{Success: false, Error: "Invalid ID"}) return } + articlesMutex.Lock() _, ind := findArticleByID(articleID) if ind != -1 { articles[ind] = articles[len(articles)-1] articles = articles[:len(articles)-1] + articlesMutex.Unlock() c.JSON(200, APIResponse{Success: true, Message: "article deleted successfully"}) } else { + articlesMutex.Unlock() c.JSON(404, APIResponse{Success: false, Error: "article not found"}) } } @@ -331,9 +343,10 @@ func getStats(c *gin.Context) { } func findArticleByID(id int) (*Article, int) { - for ind, article := range articles { - if article.ID == id { - return &article, ind + for ind := range articles { + if articles[ind].ID == id { + copyArticle := articles[ind] + return ©Article, ind } } return nil, -1 @@ -341,23 +354,7 @@ func findArticleByID(id int) (*Article, int) { func validateArticle(article Article) error { if article.Title == "" || article.Content == "" || article.Author == "" { - return errors.New("name is required") + return errors.New("title, content, and author are required") } return nil } - -func okResponse(c *gin.Context, status int, message string, data interface{}) { - c.JSON(status, APIResponse{ - Success: true, - Data: data, - Message: message, - RequestID: c.GetString("request_id"), - }) -} -func errResponse(c *gin.Context, status int, msg string) { - c.JSON(status, APIResponse{ - Success: false, - Error: msg, - RequestID: c.GetString("request_id"), - }) -} From 4a4fd168aad718c2c109b971525004de763405e7 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:49:41 +0000 Subject: [PATCH 3/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index 032c2a04..1a9fbd29 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -106,6 +106,7 @@ func LoggingMiddleware() gin.HandlerFunc { } func getUserRole(apiKey string) (bool, string) { + // these keys are default for this assignment, for production use os.Getenv("ADMIN_API_KEY") roles := map[string]string{ "admin-key-123": "admin", "user-key-456": "user"} @@ -192,6 +193,7 @@ func ContentTypeMiddleware() gin.HandlerFunc { c.Next() } } + // the assignment required to return error message, remove Message in production and use Internal Error instead func ErrorHandlerMiddleware() gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { @@ -218,7 +220,7 @@ func getArticles(c *gin.Context) { articlesMutex.RUnlock() c.JSON(200, APIResponse{ Success: true, - Data: articles, + Data: articlesTemp, Message: "all articles", RequestID: c.GetString("request_id")}) } @@ -263,8 +265,8 @@ func createArticle(c *gin.Context) { return } articlesMutex.Lock() - nextID++ newArticle.ID = nextID + nextID++ newArticle.CreatedAt = time.Now() newArticle.UpdatedAt = time.Now() articles = append(articles, newArticle) @@ -334,8 +336,11 @@ func getStats(c *gin.Context) { c.JSON(403, APIResponse{Success: false, Error: "Unauthorized"}) return } + articlesMutex.RLock() + totalArticles := len(articles) + articlesMutex.RUnlock() stats := map[string]interface{}{ - "total_articles": len(articles), + "total_articles": totalArticles, "total_requests": 10, "uptime": "24h", } From f393b50b462ed0904ce65c81ec20c6a30182c061 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:05:02 +0000 Subject: [PATCH 4/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index 1a9fbd29..ea1e0af3 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -156,6 +156,7 @@ func CORSMiddleware() gin.HandlerFunc { } } +// with no eviction mechanism. In a production scenario or under attack, this would consume unbounded memory. func RateLimitMiddleware() gin.HandlerFunc { return func(c *gin.Context) { ip := c.ClientIP() @@ -293,6 +294,7 @@ func updateArticle(c *gin.Context) { return } articlesMutex.Lock() + defer articlesMutex.Unlock() article, ind := findArticleByID(articleID) if ind != -1 { article.Author = newArticle.Author @@ -300,13 +302,11 @@ func updateArticle(c *gin.Context) { article.Title = newArticle.Title article.UpdatedAt = time.Now() articles[ind] = *article // Persist back to slice - articlesMutex.Unlock() c.JSON(200, APIResponse{ Success: true, Data: article, Message: "Article updated successfully"}) } else { - articlesMutex.Unlock() c.JSON(404, APIResponse{Success: false, Error: "article not found"}) } } @@ -318,14 +318,13 @@ func deleteArticle(c *gin.Context) { return } articlesMutex.Lock() + defer articlesMutex.Unlock() _, ind := findArticleByID(articleID) if ind != -1 { articles[ind] = articles[len(articles)-1] articles = articles[:len(articles)-1] - articlesMutex.Unlock() c.JSON(200, APIResponse{Success: true, Message: "article deleted successfully"}) } else { - articlesMutex.Unlock() c.JSON(404, APIResponse{Success: false, Error: "article not found"}) } } @@ -333,7 +332,7 @@ func deleteArticle(c *gin.Context) { // getStats handles GET /admin/stats - get API usage statistics (admin only) func getStats(c *gin.Context) { if c.GetString("user_role") != "admin" { - c.JSON(403, APIResponse{Success: false, Error: "Unauthorized"}) + c.JSON(403, APIResponse{Success: false, Error: "Forbidden: admin access required"}) return } articlesMutex.RLock() From e5e62707ca049fdcc3f9e406b427e761784a8b23 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:13:54 +0000 Subject: [PATCH 5/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index ea1e0af3..ee61afd1 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "os" "strconv" "strings" "sync" @@ -35,15 +36,35 @@ var articles = []Article{ {ID: 1, Title: "Getting Started with Go", Content: "Go is a programming language", Author: "John Doe", CreatedAt: time.Now(), UpdatedAt: time.Now()}, {ID: 2, Title: "Web Development with Gin", Content: "Gin is a web framework", Author: "Jane Smith", CreatedAt: time.Now(), UpdatedAt: time.Now()}, } + +type clientLimiter struct { + limiter *rate.Limiter + lastSeen time.Time +} + var ( nextID = 3 articlesMutex sync.RWMutex - rateLimiters = make(map[string]*rate.Limiter) + rateLimiters = make(map[string]*clientLimiter) rateLimitMutex sync.Mutex ) func main() { router := gin.New() + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + cutoff := time.Now().Add(-10 * time.Minute) + rateLimitMutex.Lock() + for ip, entry := range rateLimiters { + if entry.lastSeen.Before(cutoff) { + delete(rateLimiters, ip) + } + } + rateLimitMutex.Unlock() + } + }() router.Use( RequestIDMiddleware(), ErrorHandlerMiddleware(), @@ -106,10 +127,18 @@ func LoggingMiddleware() gin.HandlerFunc { } func getUserRole(apiKey string) (bool, string) { - // these keys are default for this assignment, for production use os.Getenv("ADMIN_API_KEY") + adminKey := os.Getenv("ADMIN_API_KEY") + if adminKey == "" { + adminKey = "admin-key-123" + } + userKey := os.Getenv("USER_API_KEY") + if userKey == "" { + userKey = "user-key-456" + } roles := map[string]string{ - "admin-key-123": "admin", - "user-key-456": "user"} + adminKey: "admin", + userKey: "user", + } val, prs := roles[apiKey] if prs { return true, val @@ -120,13 +149,13 @@ func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { apiKey := c.GetHeader("X-API-Key") if apiKey == "" { - c.JSON(401, APIResponse{Success: false, Error: "API key required"}) + c.JSON(401, APIResponse{Success: false, Error: "API key required", RequestID: c.GetString("request_id")}) c.Abort() return } isValid, userRole := getUserRole(apiKey) if !isValid { - c.JSON(401, APIResponse{Success: false, Error: "Invalid API key"}) + c.JSON(401, APIResponse{Success: false, Error: "Invalid API key", RequestID: c.GetString("request_id")}) c.Abort() return } @@ -156,16 +185,22 @@ func CORSMiddleware() gin.HandlerFunc { } } -// with no eviction mechanism. In a production scenario or under attack, this would consume unbounded memory. func RateLimitMiddleware() gin.HandlerFunc { return func(c *gin.Context) { ip := c.ClientIP() + now := time.Now() rateLimitMutex.Lock() - limiter, ok := rateLimiters[ip] + entry, ok := rateLimiters[ip] if !ok { - limiter = rate.NewLimiter(rate.Every(time.Minute/100), 100) - rateLimiters[ip] = limiter + entry = &clientLimiter{ + limiter: rate.NewLimiter(rate.Every(time.Minute/100), 100), + lastSeen: now, + } + rateLimiters[ip] = entry + } else { + entry.lastSeen = now } + limiter := entry.limiter rateLimitMutex.Unlock() c.Writer.Header().Set("X-RateLimit-Limit", "100") c.Writer.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) @@ -257,12 +292,12 @@ func getArticle(c *gin.Context) { func createArticle(c *gin.Context) { var newArticle Article if err := c.ShouldBindJSON(&newArticle); err != nil { - c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + c.JSON(400, APIResponse{Success: false, Error: err.Error(), RequestID: c.GetString("request_id")}) return } if err := validateArticle(newArticle); err != nil { - c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + c.JSON(400, APIResponse{Success: false, Error: err.Error(), RequestID: c.GetString("request_id")}) return } articlesMutex.Lock() @@ -272,7 +307,7 @@ func createArticle(c *gin.Context) { newArticle.UpdatedAt = time.Now() articles = append(articles, newArticle) articlesMutex.Unlock() - c.JSON(201, APIResponse{Success: true, Data: newArticle, Message: "Article created"}) + c.JSON(201, APIResponse{Success: true, Data: newArticle, Message: "Article created", RequestID: c.GetString("request_id")}) } From 545b512d7fef56e861d8d8deb309340f426277d3 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:43:35 +0000 Subject: [PATCH 6/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index ee61afd1..bb9a55ef 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -129,11 +129,11 @@ func LoggingMiddleware() gin.HandlerFunc { func getUserRole(apiKey string) (bool, string) { adminKey := os.Getenv("ADMIN_API_KEY") if adminKey == "" { - adminKey = "admin-key-123" + adminKey = "admin-key-123" // this value is part of assignment unit test } userKey := os.Getenv("USER_API_KEY") if userKey == "" { - userKey = "user-key-456" + userKey = "user-key-456" // this value is part of assignment unit test } roles := map[string]string{ adminKey: "admin", @@ -230,14 +230,17 @@ func ContentTypeMiddleware() gin.HandlerFunc { } } -// the assignment required to return error message, remove Message in production and use Internal Error instead func ErrorHandlerMiddleware() gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { log.Printf("Panic recovered: %v", recovered) + message := "" + if gin.Mode() != gin.ReleaseMode { + message = fmt.Sprintf("%v", recovered) + } c.JSON(http.StatusInternalServerError, APIResponse{ Success: false, Error: "Internal server error", - Message: fmt.Sprintf("%v", recovered), + Message: message, RequestID: c.GetString("request_id"), }) c.Abort() @@ -315,17 +318,17 @@ func updateArticle(c *gin.Context) { id := c.Param("id") articleID, err := strconv.Atoi(id) if err != nil { - c.JSON(400, APIResponse{Success: false, Error: "Invalid ID"}) + c.JSON(400, APIResponse{Success: false, Error: "Invalid ID", RequestID: c.GetString("request_id")}) return } var newArticle Article if err := c.ShouldBindJSON(&newArticle); err != nil { - c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + c.JSON(400, APIResponse{Success: false, Error: err.Error(), RequestID: c.GetString("request_id")}) return } if err := validateArticle(newArticle); err != nil { - c.JSON(400, APIResponse{Success: false, Error: err.Error()}) + c.JSON(400, APIResponse{Success: false, Error: err.Error(), RequestID: c.GetString("request_id")}) return } articlesMutex.Lock() @@ -338,11 +341,12 @@ func updateArticle(c *gin.Context) { article.UpdatedAt = time.Now() articles[ind] = *article // Persist back to slice c.JSON(200, APIResponse{ - Success: true, - Data: article, - Message: "Article updated successfully"}) + Success: true, + Data: article, + Message: "Article updated successfully", + RequestID: c.GetString("request_id")}) } else { - c.JSON(404, APIResponse{Success: false, Error: "article not found"}) + c.JSON(404, APIResponse{Success: false, Error: "article not found", RequestID: c.GetString("request_id")}) } } func deleteArticle(c *gin.Context) { From 4c307349986f60e355f941269dacdc56803fbeb9 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:06:18 +0000 Subject: [PATCH 7/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index bb9a55ef..20c535dd 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -127,13 +127,14 @@ func LoggingMiddleware() gin.HandlerFunc { } func getUserRole(apiKey string) (bool, string) { + // this function is fixed for this assignment. no change is possible. adminKey := os.Getenv("ADMIN_API_KEY") + userKey := os.Getenv("USER_API_KEY") if adminKey == "" { - adminKey = "admin-key-123" // this value is part of assignment unit test + adminKey = "admin-key-123" } - userKey := os.Getenv("USER_API_KEY") if userKey == "" { - userKey = "user-key-456" // this value is part of assignment unit test + userKey = "user-key-456" } roles := map[string]string{ adminKey: "admin", @@ -203,7 +204,7 @@ func RateLimitMiddleware() gin.HandlerFunc { limiter := entry.limiter rateLimitMutex.Unlock() c.Writer.Header().Set("X-RateLimit-Limit", "100") - c.Writer.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) + c.Writer.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(time.Minute).Unix())) // this header is part of the assignment and cannot be removed if !limiter.Allow() { c.Writer.Header().Set("X-RateLimit-Remaining", "0") c.JSON(http.StatusTooManyRequests, APIResponse{Success: false, Error: "rate limit exceeded"}) @@ -353,7 +354,7 @@ func deleteArticle(c *gin.Context) { id := c.Param("id") articleID, err := strconv.Atoi(id) if err != nil { - c.JSON(400, APIResponse{Success: false, Error: "Invalid ID"}) + c.JSON(400, APIResponse{Success: false, Error: "Invalid ID", RequestID: c.GetString("request_id")}) return } articlesMutex.Lock() @@ -362,9 +363,9 @@ func deleteArticle(c *gin.Context) { if ind != -1 { articles[ind] = articles[len(articles)-1] articles = articles[:len(articles)-1] - c.JSON(200, APIResponse{Success: true, Message: "article deleted successfully"}) + c.JSON(200, APIResponse{Success: true, Message: "article deleted successfully", RequestID: c.GetString("request_id")}) } else { - c.JSON(404, APIResponse{Success: false, Error: "article not found"}) + c.JSON(404, APIResponse{Success: false, Error: "article not found", RequestID: c.GetString("request_id")}) } } From ea59a00e199683190a28103b98f71371bd2c0557 Mon Sep 17 00:00:00 2001 From: "go-interview-practice-bot[bot]" <230190823+go-interview-practice-bot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:17:39 +0000 Subject: [PATCH 8/8] Add solution for gin challenge-2-middleware --- .../submissions/imankhodadi/solution.go | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go index 20c535dd..374b3e11 100644 --- a/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go +++ b/packages/gin/challenge-2-middleware/submissions/imankhodadi/solution.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "log" @@ -50,21 +51,28 @@ var ( ) func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() router := gin.New() - go func() { + go func(ctx context.Context) { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() - for range ticker.C { - cutoff := time.Now().Add(-10 * time.Minute) - rateLimitMutex.Lock() - for ip, entry := range rateLimiters { - if entry.lastSeen.Before(cutoff) { - delete(rateLimiters, ip) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + cutoff := time.Now().Add(-10 * time.Minute) + rateLimitMutex.Lock() + for ip, entry := range rateLimiters { + if entry.lastSeen.Before(cutoff) { + delete(rateLimiters, ip) + } } + rateLimitMutex.Unlock() } - rateLimitMutex.Unlock() } - }() + }(ctx) router.Use( RequestIDMiddleware(), ErrorHandlerMiddleware(), @@ -372,7 +380,7 @@ func deleteArticle(c *gin.Context) { // getStats handles GET /admin/stats - get API usage statistics (admin only) func getStats(c *gin.Context) { if c.GetString("user_role") != "admin" { - c.JSON(403, APIResponse{Success: false, Error: "Forbidden: admin access required"}) + c.JSON(403, APIResponse{Success: false, Error: "Forbidden: admin access required", RequestID: c.GetString("request_id")}) return } articlesMutex.RLock() @@ -383,7 +391,7 @@ func getStats(c *gin.Context) { "total_requests": 10, "uptime": "24h", } - c.JSON(200, APIResponse{Success: true, Data: stats, Message: "stats"}) + c.JSON(200, APIResponse{Success: true, Data: stats, Message: "stats", RequestID: c.GetString("request_id")}) } func findArticleByID(id int) (*Article, int) {