package middleware import ( "errors" "fmt" "net/http" "strings" "time" "service-warungpos-go/config" "service-warungpos-go/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type JWTClaims struct { Sub uint `json:"sub"` Role string `json:"role"` CompanyID *uint `json:"company_id"` SessionToken string `json:"session_token,omitempty"` jwt.RegisteredClaims } // Security Helpers func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } func VerifyPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } func CreateAccessToken(userID uint, role string, companyID *uint, sessionToken ...string) (string, error) { expiryHours := config.GlobalConfig.JWTExpiryHours if expiryHours == 0 { expiryHours = 24 } claims := JWTClaims{ Sub: userID, Role: role, CompanyID: companyID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiryHours) * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } if len(sessionToken) > 0 && sessionToken[0] != "" { claims.SessionToken = sessionToken[0] } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.GlobalConfig.JWTSecretKey)) } func ParseToken(tokenString string) (*JWTClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(config.GlobalConfig.JWTSecretKey), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token claims") } // Gin Middlewares func GetCurrentUser() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Token otorisasi diperlukan"}) c.Abort() return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Format token otorisasi salah"}) c.Abort() return } tokenString := parts[1] claims, err := ParseToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"detail": fmt.Sprintf("Token error: %v", err)}) c.Abort() return } var user models.User if err := config.DB.Select("id, name, email, phone, role, company_id, branch_id, is_active, created_at").First(&user, claims.Sub).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"detail": "User tidak ditemukan"}) c.Abort() return } if !user.IsActive { c.JSON(http.StatusForbidden, gin.H{"detail": "Akun Anda dinonaktifkan. Hubungi Administrator."}) c.Abort() return } // Cek single session enforcement untuk kasir if user.Role == "kasir" && user.CompanyID != nil { var company models.Company if err := config.DB.Select("enforce_single_session").First(&company, *user.CompanyID).Error; err == nil { if company.EnforceSingleSession { // Harus ada session_token di JWT if claims.SessionToken == "" { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Sesi tidak valid, silakan login ulang.", "code": "SESSION_EXPIRED"}) c.Abort() return } // Validasi session_token ke DB var session models.UserSession err := config.DB.Where( "user_id = ? AND session_token = ? AND expires_at > ?", user.ID, claims.SessionToken, time.Now(), ).First(&session).Error if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Sesi Anda sudah tidak aktif. Perangkat lain telah login dengan akun ini.", "code": "SESSION_EXPIRED"}) c.Abort() return } } } } // Simpan user ke context Gin c.Set("user", &user) c.Next() } } func RequireSuperAdmin() gin.HandlerFunc { return func(c *gin.Context) { userVal, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Hubungan otorisasi gagal"}) c.Abort() return } user := userVal.(*models.User) if user.Role != "super_admin" { c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak: Hanya super_admin yang memiliki izin."}) c.Abort() return } c.Next() } } func RequireOwner() gin.HandlerFunc { return func(c *gin.Context) { userVal, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Hubungan otorisasi gagal"}) c.Abort() return } user := userVal.(*models.User) if user.Role != "owner" && user.Role != "super_admin" { // Super admin biasakan lolos untuk CRUD company c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak: Hanya owner yang memiliki izin."}) c.Abort() return } if user.CompanyID == nil { c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak: Akun Anda tidak terikat ke perusahaan/toko manapun."}) c.Abort() return } c.Next() } } func RequireTenantContext() gin.HandlerFunc { return func(c *gin.Context) { userVal, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Hubungan otorisasi gagal"}) c.Abort() return } user := userVal.(*models.User) if user.CompanyID == nil { c.JSON(http.StatusForbidden, gin.H{"detail": "Akses ditolak: User tidak terikat ke perusahaan/toko manapun."}) c.Abort() return } c.Next() } } func RequireAuthenticated() gin.HandlerFunc { return func(c *gin.Context) { _, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Hubungan otorisasi gagal"}) c.Abort() return } c.Next() } }