This commit is contained in:
aditya.siregar
2025-07-18 20:10:29 +07:00
parent 1bceae010b
commit 4f5950543e
511 changed files with 24132 additions and 34137 deletions
+141
View File
@@ -0,0 +1,141 @@
package middleware
import (
"apskel-pos-be/internal/appcontext"
"net/http"
"strings"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/service"
"github.com/gin-gonic/gin"
)
type AuthMiddleware struct {
authService service.AuthService
}
func NewAuthMiddleware(authService service.AuthService) *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.UserRoleKey, userResponse.Role)
setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String())
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
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) 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)
}
+4
View File
@@ -0,0 +1,4 @@
package middleware
type AuthProcessor interface {
}
+67
View File
@@ -0,0 +1,67 @@
package middleware
import (
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants"
"context"
"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)
}
+22
View File
@@ -0,0 +1,22 @@
package middleware
import (
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants"
"context"
"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()
}
}
+21
View File
@@ -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()
})
}
+17
View File
@@ -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()
}
}
+24
View File
@@ -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,
)
})
}
+69
View File
@@ -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()
})
}
+30
View File
@@ -0,0 +1,30 @@
package middleware
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-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()
}
}
+35
View File
@@ -0,0 +1,35 @@
package middleware
import (
"apskel-pos-be/internal/constants"
"apskel-pos-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()),
})
}
}
+64
View File
@@ -0,0 +1,64 @@
package middleware
import (
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/models"
"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) {
//tokenString := c.GetHeader("Authorization")
//if tokenString == "" {
// c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
// c.Abort()
// return
//}
//
//ctx := c.Request.Context()
//
//user, err := uir.resolveUserID(c, gopayAccountID)
//if userID == "" {
// logger.FromContext(c.Request.Context()).Error("UserIDResolver::Handle -> userID could not be resolved")
// errorResponse := contract.BuildErrorResponse([]*contract.ResponseError{
// contract.NewResponseError(constants.InternalServerErrorCode, "user-id", "failed to resolve user id from gopay account id"),
// })
// util.WriteResponse(c.Writer, c.Request, *errorResponse, http.StatusInternalServerError, "UserIDResolver::Handle")
// c.Abort()
// return
//}
//
//setKeyInContext(c, appcontext.UserIDKey, user.ID.String())
//setKeyInContext(c, appcontext.OrganizationIDKey, user.OrganizationID.String())
//setKeyInContext(c, appcontext.OutletIDKey, user.OutletID.String())
//setKeyInContext(c, appcontext.RoleIDKey, string(user.Role))
c.Next()
}
}
func (uir *UserIDResolver) resolveUserID(c *gin.Context, userID uuid.UUID) (*models.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 ""
}
+11
View File
@@ -0,0 +1,11 @@
package middleware
import (
"apskel-pos-be/internal/models"
"context"
"github.com/google/uuid"
)
type UserProcessor interface {
GetUserByID(ctx context.Context, id uuid.UUID) (*models.UserResponse, error)
}