Dev / middleware /auth.go
Mhamdans17
feat: single session enforcement + UI improvements
c6e89e6
Raw
History Blame Contribute Delete
6.02 kB
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()
}
}