Init Eslogad
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"eslogad-be/internal/appcontext"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"eslogad-be/internal/constants"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
authService AuthValidateService
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(authService AuthValidateService) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := m.extractTokenFromHeader(c)
|
||||
if token == "" {
|
||||
logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireAuth -> Missing authorization token")
|
||||
m.sendErrorResponse(c, "Authorization token is required", http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userResponse, err := m.authService.ValidateToken(token)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("AuthMiddleware::RequireAuth -> Invalid token")
|
||||
m.sendErrorResponse(c, "Invalid or expired token", http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
|
||||
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
|
||||
c.Set("user_roles", roles)
|
||||
c.Set("user_permissions", perms)
|
||||
}
|
||||
|
||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||
|
||||
hasRequiredRole := false
|
||||
for _, role := range allowedRoles {
|
||||
if appCtx.UserRole == role {
|
||||
hasRequiredRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRequiredRole {
|
||||
m.sendErrorResponse(c, "Insufficient permissions", http.StatusForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc {
|
||||
return m.RequireRole("admin", "manager")
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||||
return m.RequireRole("admin")
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireSuperAdmin() gin.HandlerFunc {
|
||||
return m.RequireRole("superadmin")
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequireActiveUser() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userResponse, exists := c.Get("user")
|
||||
if !exists {
|
||||
logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireActiveUser -> User not authenticated")
|
||||
m.sendErrorResponse(c, "Authentication required", http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := userResponse.(*contract.UserResponse)
|
||||
if !ok {
|
||||
logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireActiveUser -> Invalid user context")
|
||||
m.sendErrorResponse(c, "Invalid user context", http.StatusInternalServerError)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
logger.FromContext(c.Request.Context()).Errorf("AuthMiddleware::RequireActiveUser -> User account is deactivated: %s", user.Email)
|
||||
m.sendErrorResponse(c, "User account is deactivated", http.StatusForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireActiveUser -> Active user check passed: %s", user.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) RequirePermissions(required ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if _, exists := c.Get("user_permissions"); !exists {
|
||||
m.sendErrorResponse(c, "Authentication required", http.StatusUnauthorized)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
permIface, _ := c.Get("user_permissions")
|
||||
perms, _ := permIface.([]string)
|
||||
userPerms := map[string]bool{}
|
||||
for _, code := range perms {
|
||||
userPerms[code] = true
|
||||
}
|
||||
|
||||
for _, need := range required {
|
||||
if !userPerms[need] {
|
||||
m.sendErrorResponse(c, "Insufficient permissions", http.StatusForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) extractTokenFromHeader(c *gin.Context) string {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
func (m *AuthMiddleware) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||
errorResponse := &contract.ErrorResponse{
|
||||
Error: "auth_error",
|
||||
Message: message,
|
||||
Code: statusCode,
|
||||
Details: map[string]interface{}{
|
||||
"entity": constants.AuthHandlerEntity,
|
||||
},
|
||||
}
|
||||
c.JSON(statusCode, errorResponse)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package middleware
|
||||
|
||||
type AuthProcessor interface {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
)
|
||||
|
||||
type AuthValidateService interface {
|
||||
ValidateToken(tokenString string) (*contract.UserResponse, error)
|
||||
RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error)
|
||||
Logout(ctx context.Context, tokenString string) error
|
||||
ExtractAccess(tokenString string) (roles []string, permissions []string, err error)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/constants"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PopulateContext() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
setKeyInContext(c, appcontext.AppIDKey, getAppID(c))
|
||||
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
|
||||
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(c))
|
||||
setKeyInContext(c, appcontext.OutletIDKey, getOutletID(c))
|
||||
setKeyInContext(c, appcontext.DeviceOSKey, getDeviceOS(c))
|
||||
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
|
||||
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func getAppID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.XAppIDHeader)
|
||||
}
|
||||
|
||||
func getAppType(c *gin.Context) string {
|
||||
return c.GetHeader(constants.XAppTypeHeader)
|
||||
}
|
||||
|
||||
func getAppVersion(c *gin.Context) string {
|
||||
return c.GetHeader(constants.XAppVersionHeader)
|
||||
}
|
||||
|
||||
func getOrganizationID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OrganizationID)
|
||||
}
|
||||
|
||||
func getOutletID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OutletID)
|
||||
}
|
||||
|
||||
func getDeviceOS(c *gin.Context) string {
|
||||
return c.GetHeader(constants.XDeviceOSHeader)
|
||||
}
|
||||
|
||||
func getDevicePlatform(c *gin.Context) string {
|
||||
return c.GetHeader(constants.XPlatformHeader)
|
||||
}
|
||||
|
||||
func getUserLocale(c *gin.Context) string {
|
||||
userLocale := c.GetHeader(constants.XUserLocaleHeader)
|
||||
if userLocale == "" {
|
||||
userLocale = c.GetHeader(constants.AcceptedLanguageHeader)
|
||||
}
|
||||
if userLocale == "" {
|
||||
userLocale = c.GetHeader(constants.LocaleHeader)
|
||||
}
|
||||
return userLocale
|
||||
}
|
||||
|
||||
func setKeyInContext(c *gin.Context, contextKey interface{}, contextKeyValue string) {
|
||||
ctx := context.WithValue(c.Request.Context(),
|
||||
contextKey, contextKeyValue)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/constants"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func CorrelationID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
correlationID := c.GetHeader(constants.CorrelationIDHeader)
|
||||
if correlationID == "" {
|
||||
correlationID = uuid.New().String()
|
||||
}
|
||||
ctx := context.WithValue(c.Request.Context(), appcontext.CorrelationIDKey, correlationID)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Writer.Header().Set(constants.CorrelationIDHeader, correlationID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return gin.HandlerFunc(func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeHeader = "Content-Type"
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
func JsonAPI() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set(contentTypeHeader, jsonContentType)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Logging() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format(time.RFC1123),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.Request.UserAgent(),
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
requests map[string][]time.Time
|
||||
mutex sync.RWMutex
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
requests: make(map[string][]time.Time),
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Allow(key string) bool {
|
||||
rl.mutex.Lock()
|
||||
defer rl.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
windowStart := now.Add(-rl.window)
|
||||
|
||||
// Clean old requests
|
||||
if times, exists := rl.requests[key]; exists {
|
||||
var validTimes []time.Time
|
||||
for _, t := range times {
|
||||
if t.After(windowStart) {
|
||||
validTimes = append(validTimes, t)
|
||||
}
|
||||
}
|
||||
rl.requests[key] = validTimes
|
||||
}
|
||||
|
||||
// Check if limit exceeded
|
||||
if len(rl.requests[key]) >= rl.limit {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add current request
|
||||
rl.requests[key] = append(rl.requests[key], now)
|
||||
return true
|
||||
}
|
||||
|
||||
func RateLimit() gin.HandlerFunc {
|
||||
limiter := NewRateLimiter(100, time.Minute) // 100 requests per minute
|
||||
|
||||
return gin.HandlerFunc(func(c *gin.Context) {
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if !limiter.Allow(clientIP) {
|
||||
c.JSON(429, gin.H{
|
||||
"error": "Rate limit exceeded",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/logger"
|
||||
"eslogad-be/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func Recover() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.NonContext.Errorf(nil, "Recovered from panic %v", map[string]interface{}{
|
||||
"stack_trace": string(debug.Stack()),
|
||||
"error": err,
|
||||
})
|
||||
debug.PrintStack()
|
||||
errorResponse := contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError("900", "", string(debug.Stack())),
|
||||
})
|
||||
util.WriteResponse(c.Writer, c.Request, *errorResponse, http.StatusInternalServerError, "Middleware::Recover")
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"eslogad-be/internal/constants"
|
||||
"eslogad-be/internal/logger"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func HTTPStatLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/health" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start)
|
||||
|
||||
status := c.Writer.Status()
|
||||
|
||||
log := logger.NewContextLogger(c, "HTTPStatLogger")
|
||||
log.Infof("CompletedHTTPRequest %v", map[string]string{
|
||||
constants.RequestMethod: c.Request.Method,
|
||||
constants.RequestPath: c.Request.URL.Path,
|
||||
constants.RequestURLQueryParam: c.Request.URL.RawQuery,
|
||||
constants.ResponseStatusCode: fmt.Sprintf("%d", status),
|
||||
constants.ResponseStatusText: http.StatusText(status),
|
||||
constants.ResponseTimeTaken: fmt.Sprintf("%f", duration.Seconds()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserIDResolver struct {
|
||||
userProcessor UserProcessor
|
||||
authProcessor AuthProcessor
|
||||
}
|
||||
|
||||
func NewUserIDResolver(userProcessor UserProcessor, authProcessor AuthProcessor) *UserIDResolver {
|
||||
return &UserIDResolver{
|
||||
userProcessor: userProcessor,
|
||||
authProcessor: authProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (uir *UserIDResolver) Handle() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (uir *UserIDResolver) resolveUserID(c *gin.Context, userID uuid.UUID) (*contract.UserResponse, error) {
|
||||
user, err := uir.userProcessor.GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("UserIDResolver::resolveGopayUserID -> userID could not be resolved")
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (uir *UserIDResolver) validate(c *gin.Context, tokenString string) string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserProcessor interface {
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
}
|
||||
Reference in New Issue
Block a user