2025-09-28 01:53:09 +00:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
import (
|
2025-09-30 13:34:56 +00:00
|
|
|
"netidhub-saas-be/app/database/entity"
|
|
|
|
|
"netidhub-saas-be/utils/client"
|
2025-09-28 01:53:09 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2025-09-30 13:34:56 +00:00
|
|
|
ClientKeyHeader = "X-Client-Key"
|
|
|
|
|
ClientContextKey = "client_id"
|
|
|
|
|
UserIDContextKey = "user_id"
|
|
|
|
|
IsSuperAdminContextKey = "is_super_admin"
|
|
|
|
|
AccessibleClientIDsKey = "accessible_client_ids"
|
|
|
|
|
CurrentClientIDKey = "current_client_id"
|
2025-09-28 01:53:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// excludedPaths contains paths that don't require client key validation
|
|
|
|
|
var excludedPaths = []string{
|
|
|
|
|
"/swagger/*",
|
|
|
|
|
"/docs/*",
|
|
|
|
|
"/users/login",
|
|
|
|
|
"/health/*",
|
|
|
|
|
"/clients",
|
|
|
|
|
"/clients/*",
|
|
|
|
|
"*/viewer/*",
|
|
|
|
|
"/bookmarks/test-table",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isPathExcluded checks if the given path should be excluded from client key validation
|
|
|
|
|
func isPathExcluded(path string) bool {
|
|
|
|
|
for _, excludedPath := range excludedPaths {
|
|
|
|
|
if strings.HasPrefix(excludedPath, "*") && strings.HasSuffix(excludedPath, "*") {
|
|
|
|
|
// Handle wildcard at both beginning and end (e.g., "*/viewer/*")
|
|
|
|
|
pattern := excludedPath[1 : len(excludedPath)-1] // Remove * from both ends
|
|
|
|
|
if strings.Contains(path, pattern) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
} else if strings.HasPrefix(excludedPath, "*") {
|
|
|
|
|
// Handle wildcard at the beginning
|
|
|
|
|
if strings.HasSuffix(path, excludedPath[1:]) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
} else if strings.HasSuffix(excludedPath, "*") {
|
|
|
|
|
// Handle wildcard at the end
|
|
|
|
|
prefix := excludedPath[:len(excludedPath)-1]
|
|
|
|
|
if strings.HasPrefix(path, prefix) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Exact match
|
|
|
|
|
if path == excludedPath {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClientMiddleware extracts and validates the Client Key from request headers
|
2025-09-30 13:34:56 +00:00
|
|
|
// Enhanced to support multi-client access and super admin
|
2025-09-28 01:53:09 +00:00
|
|
|
func ClientMiddleware(db *gorm.DB) fiber.Handler {
|
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
|
|
|
// Check if path should be excluded from client key validation
|
|
|
|
|
if isPathExcluded(c.Path()) {
|
|
|
|
|
return c.Next()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// Check if user ID exists in context (set by auth middleware)
|
|
|
|
|
userID := c.Locals(UserIDContextKey)
|
|
|
|
|
if userID != nil {
|
|
|
|
|
// User authenticated - use multi-client logic
|
|
|
|
|
return handleAuthenticatedUser(c, db, userID)
|
2025-09-28 01:53:09 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// Fallback to X-Client-Key validation (backward compatibility)
|
|
|
|
|
return handleClientKeyValidation(c, db)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleAuthenticatedUser handles request from authenticated users
|
|
|
|
|
func handleAuthenticatedUser(c *fiber.Ctx, db *gorm.DB, userID interface{}) error {
|
|
|
|
|
userId, ok := userID.(uint)
|
|
|
|
|
if !ok {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 401,
|
|
|
|
|
"messages": []string{"Invalid user ID in context"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get user details
|
|
|
|
|
var user entity.Users
|
|
|
|
|
if err := db.Select("id, is_super_admin, client_id").
|
|
|
|
|
Where("id = ?", userId).
|
|
|
|
|
First(&user).Error; err != nil {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 401,
|
|
|
|
|
"messages": []string{"User not found"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store super admin status
|
|
|
|
|
isSuperAdmin := user.IsSuperAdmin != nil && *user.IsSuperAdmin
|
|
|
|
|
c.Locals(IsSuperAdminContextKey, isSuperAdmin)
|
|
|
|
|
|
|
|
|
|
// Get accessible client IDs for this user
|
|
|
|
|
accessibleClientIDs, err := client.GetAccessibleClientIDs(db, userId, isSuperAdmin)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 500,
|
|
|
|
|
"messages": []string{"Error retrieving client access"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store accessible client IDs in context
|
|
|
|
|
c.Locals(AccessibleClientIDsKey, accessibleClientIDs)
|
|
|
|
|
|
|
|
|
|
// Determine current client ID (from header or user's primary client)
|
|
|
|
|
var currentClientID *uuid.UUID
|
|
|
|
|
clientKeyHeader := c.Get(ClientKeyHeader)
|
|
|
|
|
|
|
|
|
|
if clientKeyHeader != "" {
|
|
|
|
|
// User specified a client via header
|
|
|
|
|
clientUUID, err := uuid.Parse(clientKeyHeader)
|
2025-09-28 01:53:09 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 400,
|
|
|
|
|
"messages": []string{"Invalid Client Key format"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// Verify user has access to this client
|
|
|
|
|
hasAccess, err := client.HasAccessToClient(db, userId, clientUUID, isSuperAdmin)
|
|
|
|
|
if err != nil || !hasAccess {
|
|
|
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
2025-09-28 01:53:09 +00:00
|
|
|
"success": false,
|
2025-09-30 13:34:56 +00:00
|
|
|
"code": 403,
|
|
|
|
|
"messages": []string{"Access denied to this client"},
|
2025-09-28 01:53:09 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
currentClientID = &clientUUID
|
|
|
|
|
} else if user.ClientId != nil {
|
|
|
|
|
// Use user's primary client
|
|
|
|
|
currentClientID = user.ClientId
|
|
|
|
|
}
|
2025-09-28 01:53:09 +00:00
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// Store current client ID
|
|
|
|
|
if currentClientID != nil {
|
|
|
|
|
c.Locals(CurrentClientIDKey, *currentClientID)
|
|
|
|
|
c.Locals(ClientContextKey, *currentClientID) // Backward compatibility
|
2025-09-28 01:53:09 +00:00
|
|
|
}
|
2025-09-30 13:34:56 +00:00
|
|
|
|
|
|
|
|
return c.Next()
|
2025-09-28 01:53:09 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// handleClientKeyValidation validates X-Client-Key header (backward compatibility)
|
|
|
|
|
func handleClientKeyValidation(c *fiber.Ctx, db *gorm.DB) error {
|
|
|
|
|
clientKey := c.Get(ClientKeyHeader)
|
|
|
|
|
|
|
|
|
|
if clientKey == "" {
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 400,
|
|
|
|
|
"messages": []string{"Client Key is required in header: " + ClientKeyHeader},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clientUUID, err := uuid.Parse(clientKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 400,
|
|
|
|
|
"messages": []string{"Invalid Client Key format"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var clientEntity entity.Clients
|
|
|
|
|
if err := db.Where("id = ? AND is_active = ?", clientUUID, true).First(&clientEntity).Error; err != nil {
|
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 401,
|
|
|
|
|
"messages": []string{"Invalid or inactive Client Key"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
|
|
|
"success": false,
|
|
|
|
|
"code": 500,
|
|
|
|
|
"messages": []string{"Error validating Client Key"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.Locals(ClientContextKey, clientUUID)
|
|
|
|
|
c.Locals(CurrentClientIDKey, clientUUID)
|
|
|
|
|
|
|
|
|
|
return c.Next()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetClientID retrieves the client ID from the context (backward compatibility)
|
2025-09-28 01:53:09 +00:00
|
|
|
func GetClientID(c *fiber.Ctx) *uuid.UUID {
|
|
|
|
|
if clientID, ok := c.Locals(ClientContextKey).(uuid.UUID); ok {
|
|
|
|
|
return &clientID
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:34:56 +00:00
|
|
|
// GetAccessibleClientIDs retrieves all accessible client IDs from context
|
|
|
|
|
func GetAccessibleClientIDs(c *fiber.Ctx) []uuid.UUID {
|
|
|
|
|
if clientIDs, ok := c.Locals(AccessibleClientIDsKey).([]uuid.UUID); ok {
|
|
|
|
|
return clientIDs
|
|
|
|
|
}
|
|
|
|
|
return nil // nil = super admin or no restriction
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetCurrentClientID retrieves the current working client ID from context
|
|
|
|
|
func GetCurrentClientID(c *fiber.Ctx) *uuid.UUID {
|
|
|
|
|
if clientID, ok := c.Locals(CurrentClientIDKey).(uuid.UUID); ok {
|
|
|
|
|
return &clientID
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsSuperAdmin checks if the current user is a super admin
|
|
|
|
|
func IsSuperAdmin(c *fiber.Ctx) bool {
|
|
|
|
|
if isSuperAdmin, ok := c.Locals(IsSuperAdminContextKey).(bool); ok {
|
|
|
|
|
return isSuperAdmin
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 01:53:09 +00:00
|
|
|
// AddExcludedPath adds a new path to the excluded paths list
|
|
|
|
|
func AddExcludedPath(path string) {
|
|
|
|
|
excludedPaths = append(excludedPaths, path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetExcludedPaths returns the current list of excluded paths
|
|
|
|
|
func GetExcludedPaths() []string {
|
|
|
|
|
return excludedPaths
|
|
|
|
|
}
|