Add initial project structure with core functionality for book and stationery uploads
- Created main application entry point in `main.go`. - Added configuration management in `config/config.go` and tests in `config/config_test.go`. - Implemented handlers for book and stationery uploads in `handlers/book.go` and `handlers/stationery.go`, including validation logic. - Established database connection and services in `services/database.go` and `services/book_service.go`. - Defined models for books and stationery in `models/book.go` and `models/stationery.go`. - Set up Firebase integration for image uploads in `services/firebase.go`. - Created migration scripts for database schema in `migrations/001_create_tables.sql` and subsequent updates. - Added CORS and error handling middleware. - Included comprehensive tests for handlers, services, and utilities. - Documented API endpoints and usage in `README.md` and migration instructions in `migrations/README.md`. - Introduced `.gitignore` to exclude unnecessary files and directories. - Added Go module support with `go.mod` and `go.sum` files. - Implemented utility functions for slug generation and validation in `utils/slug.go` and `utils/validation.go`.
This commit is contained in:
20
middleware/cors.go
Normal file
20
middleware/cors.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"jd-book-uploader/config"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
)
|
||||
|
||||
// SetupCORS configures CORS middleware based on application config
|
||||
func SetupCORS(cfg *config.Config) fiber.Handler {
|
||||
return cors.New(cors.Config{
|
||||
AllowOrigins: cfg.FrontendURL,
|
||||
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
AllowHeaders: "Origin,Content-Type,Accept,Authorization",
|
||||
AllowCredentials: true,
|
||||
ExposeHeaders: "Content-Length",
|
||||
MaxAge: 3600, // 1 hour
|
||||
})
|
||||
}
|
||||
42
middleware/cors_test.go
Normal file
42
middleware/cors_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"jd-book-uploader/config"
|
||||
)
|
||||
|
||||
func TestSetupCORS(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
FrontendURL: "http://localhost:5173",
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(SetupCORS(cfg))
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendString("OK")
|
||||
})
|
||||
|
||||
// Test CORS preflight request
|
||||
req := httptest.NewRequest("OPTIONS", "/test", nil)
|
||||
req.Header.Set("Origin", "http://localhost:5173")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test request failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != fiber.StatusNoContent {
|
||||
t.Errorf("Expected status %d, got %d", fiber.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Check CORS headers
|
||||
allowOrigin := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
if allowOrigin != "http://localhost:5173" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin %s, got %s", "http://localhost:5173", allowOrigin)
|
||||
}
|
||||
}
|
||||
66
middleware/error_handler.go
Normal file
66
middleware/error_handler.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ErrorHandler is a global error handler middleware
|
||||
func ErrorHandler(c *fiber.Ctx) error {
|
||||
// Call next middleware/handler
|
||||
err := c.Next()
|
||||
|
||||
// If no error, return
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log error with context
|
||||
log.Printf("Error: %v | Method: %s | Path: %s | IP: %s",
|
||||
err,
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
c.IP(),
|
||||
)
|
||||
|
||||
// Check if error is a Fiber error
|
||||
code := fiber.StatusInternalServerError
|
||||
message := "Internal Server Error"
|
||||
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
message = e.Message
|
||||
}
|
||||
|
||||
// Return structured error response
|
||||
return c.Status(code).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RecoverHandler recovers from panics and returns a proper error response
|
||||
func RecoverHandler() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Log panic with context
|
||||
log.Printf("Panic recovered: %v | Method: %s | Path: %s | IP: %s",
|
||||
r,
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
c.IP(),
|
||||
)
|
||||
|
||||
// Return error response
|
||||
c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Internal Server Error",
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
63
middleware/error_handler_test.go
Normal file
63
middleware/error_handler_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestErrorHandler(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(ErrorHandler)
|
||||
|
||||
app.Get("/error", func(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Bad Request")
|
||||
})
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
return errors.New("some error")
|
||||
})
|
||||
|
||||
// Test Fiber error
|
||||
req := httptest.NewRequest("GET", "/error", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test request failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != fiber.StatusBadRequest {
|
||||
t.Errorf("Expected status %d, got %d", fiber.StatusBadRequest, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Test generic error
|
||||
req = httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err = app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test request failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != fiber.StatusInternalServerError {
|
||||
t.Errorf("Expected status %d, got %d", fiber.StatusInternalServerError, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverHandler(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Use(RecoverHandler())
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("test panic")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test request failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != fiber.StatusInternalServerError {
|
||||
t.Errorf("Expected status %d, got %d", fiber.StatusInternalServerError, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
44
middleware/logger.go
Normal file
44
middleware/logger.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
// SetupLogger configures request logging middleware
|
||||
func SetupLogger() fiber.Handler {
|
||||
return logger.New(logger.Config{
|
||||
Format: "${time} ${status} - ${latency} ${method} ${path} ${ip}\n",
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
TimeZone: "Local",
|
||||
Output: nil, // Use default (stdout)
|
||||
})
|
||||
}
|
||||
|
||||
// CustomLogger is a more detailed logger with structured output
|
||||
func CustomLogger() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
start := time.Now()
|
||||
|
||||
// Process request
|
||||
err := c.Next()
|
||||
|
||||
// Calculate latency
|
||||
latency := time.Since(start)
|
||||
|
||||
// Log request details
|
||||
log.Printf("[%s] %s %s | Status: %d | Latency: %v | IP: %s",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
c.Response().StatusCode(),
|
||||
latency,
|
||||
c.IP(),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user