kontenhumas-be/docs/MULTI_CLIENT_ACCESS_GUIDE.md

10 KiB

Multi-Client Access & Client Hierarchy Guide

📋 Overview

Sistem ini mendukung hierarchical multi-tenancy dengan fitur:

  • Parent-Child Client relationships (unlimited depth)
  • User dapat memiliki akses ke multiple clients
  • Super Admin dengan akses ke semua clients
  • Fine-grained access control (read, write, admin, owner)
  • Automatic sub-client inheritance

🏗️ Arsitektur

Client Types

  1. Standalone Client - Client mandiri tanpa parent/child
  2. Parent Client - Client yang memiliki sub-clients
  3. Sub Client - Client yang berada di bawah parent client

User Types

  1. Super Admin - Platform administrator, akses ke SEMUA clients
  2. Multi-Client User - User dengan akses ke beberapa clients (misal: manager regional)
  3. Single-Client User - User regular dengan akses ke 1 client saja

🗄️ Database Schema

Entity: Clients

type Clients struct {
    ID             uuid.UUID  
    Name           string     
    Description    *string    
    ClientType     string     // 'parent_client', 'sub_client', 'standalone'
    ParentClientId *uuid.UUID // Reference to parent
    
    Settings       *string    // JSONB custom settings
    MaxUsers       *int       
    MaxStorage     *int64     
    
    CreatedById    *uint      
    IsActive       *bool      
}

Entity: UserClientAccess (Many-to-Many)

type UserClientAccess struct {
    ID                uint       
    UserId            uint       
    ClientId          uuid.UUID  
    
    AccessType        string     // 'read', 'write', 'admin', 'owner'
    CanManage         *bool      // Can manage client settings
    CanDelegate       *bool      // Can give access to other users
    IncludeSubClients *bool      // Auto-access to all sub-clients
    
    GrantedById       *uint      
    IsActive          *bool      
}

Entity: Users (Updated)

type Users struct {
    // ... existing fields
    
    IsSuperAdmin   *bool               // Platform super admin
    ClientId       *uuid.UUID          // Primary client
    ClientAccesses []UserClientAccess  // Multiple client access
}

🚀 Migration Steps

Step 1: Run Database Migration

# Migration akan otomatis dijalankan saat aplikasi start
# Pastikan config migrate = true di config.toml

go run main.go

Step 2: Migrate Existing Data

-- 1. Set semua existing clients sebagai 'standalone'
UPDATE clients 
SET client_type = 'standalone' 
WHERE client_type IS NULL;

-- 2. Create super admin user (contoh)
UPDATE users 
SET is_super_admin = true 
WHERE id = 1; -- Ganti dengan ID admin utama Anda

-- 3. Migrasi user existing ke UserClientAccess (optional)
-- Untuk user yang perlu akses ke multiple clients
INSERT INTO user_client_access (user_id, client_id, access_type, can_manage, is_active, created_at, updated_at)
SELECT 
    id as user_id,
    client_id,
    'admin' as access_type,
    true as can_manage,
    true as is_active,
    NOW(),
    NOW()
FROM users 
WHERE client_id IS NOT NULL 
AND is_super_admin = false;

Step 3: Update Code

Update di config/webserver/webserver.config.go:

// Ganti middleware lama
// app.Use(middleware.ClientMiddleware(db.DB))

// Dengan middleware baru
app.Use(middleware.ClientMiddlewareV2(db.DB))

Option B: Bertahap (Backward Compatible)

Kedua middleware bisa berjalan bersamaan:

// Keep old middleware for backward compatibility
app.Use(middleware.ClientMiddleware(db.DB))

// Routes baru bisa pakai V2
apiV2 := app.Group("/api/v2")
apiV2.Use(middleware.ClientMiddlewareV2(db.DB))

💻 Contoh Penggunaan

1. Setup Parent-Child Clients

// Create Parent Client
parentClient := entity.Clients{
    ID:          uuid.New(),
    Name:        "Polda Metro Jaya",
    ClientType:  "parent_client",
    IsActive:    &trueVal,
}
db.Create(&parentClient)

// Create Sub Clients
subClient1 := entity.Clients{
    ID:             uuid.New(),
    Name:           "Polres Jakarta Pusat",
    ClientType:     "sub_client",
    ParentClientId: &parentClient.ID,
    IsActive:       &trueVal,
}
db.Create(&subClient1)

subClient2 := entity.Clients{
    ID:             uuid.New(),
    Name:           "Polres Jakarta Barat",
    ClientType:     "sub_client",
    ParentClientId: &parentClient.ID,
    IsActive:       &trueVal,
}
db.Create(&subClient2)

2. Grant Multi-Client Access

import "netidhub-saas-be/app/database/entity"

// User bisa manage multiple clients
access1 := entity.UserClientAccess{
    UserId:            managerUser.ID,
    ClientId:          parentClient.ID,
    AccessType:        "admin",
    CanManage:         &trueVal,
    IncludeSubClients: &trueVal, // Auto-access semua sub-clients
    IsActive:          &trueVal,
}
db.Create(&access1)

access2 := entity.UserClientAccess{
    UserId:     managerUser.ID,
    ClientId:   anotherClient.ID,
    AccessType: "read",
    IsActive:   &trueVal,
}
db.Create(&access2)

3. Update Repository dengan Multi-Client Filter

OLD CODE:

func (r *Repository) GetAll(clientId *uuid.UUID, req request.QueryRequest) {
    query := r.DB.Model(&entity.Articles{})
    
    if clientId != nil {
        query = query.Where("client_id = ?", clientId)
    }
    // ... rest of code
}

NEW CODE:

import middlewareUtils "netidhub-saas-be/utils/middleware"

func (r *Repository) GetAll(c *fiber.Ctx, req request.QueryRequest) {
    query := r.DB.Model(&entity.Articles{})
    
    // Automatically filter by accessible clients
    query = middlewareUtils.AddMultiClientFilter(query, c)
    
    // ... rest of code
}

4. Check User Access di Controller

import (
    customMiddleware "netidhub-saas-be/app/middleware"
    clientUtils "netidhub-saas-be/utils/client"
)

func (ctrl *Controller) GetArticle(c *fiber.Ctx) error {
    articleId := c.Params("id")
    
    // Get user info
    userId := c.Locals(customMiddleware.UserIDContextKey).(uint)
    isSuperAdmin := customMiddleware.IsSuperAdmin(c)
    
    // Get article
    var article entity.Articles
    db.First(&article, articleId)
    
    // Check access
    hasAccess, _ := clientUtils.HasAccessToClient(
        db, 
        userId, 
        *article.ClientId, 
        isSuperAdmin,
    )
    
    if !hasAccess {
        return c.Status(403).JSON(fiber.Map{
            "error": "Access denied",
        })
    }
    
    return c.JSON(article)
}

5. Get Client Hierarchy

import clientUtils "netidhub-saas-be/utils/client"

func (ctrl *Controller) GetClientInfo(c *fiber.Ctx) error {
    clientId := c.Params("id")
    clientUUID, _ := uuid.Parse(clientId)
    
    // Get full hierarchy
    client, err := clientUtils.GetClientHierarchy(db, clientUUID)
    
    return c.JSON(fiber.Map{
        "client":      client,
        "parent":      client.ParentClient,
        "sub_clients": client.SubClients,
    })
}

🎯 Use Cases

Use Case 1: Regional Manager

Scenario: Manager regional yang mengawasi 3 Polres

// Grant access dengan IncludeSubClients
access := entity.UserClientAccess{
    UserId:            regionalManagerId,
    ClientId:          parentPolresId,
    AccessType:        "admin",
    IncludeSubClients: &trueVal, // Auto-akses semua polres di bawahnya
    CanManage:         &trueVal,
    IsActive:          &trueVal,
}

Use Case 2: Super Admin Dashboard

func (ctrl *AdminController) GetAllArticles(c *fiber.Ctx) error {
    // Super admin bisa lihat semua artikel dari semua clients
    if !customMiddleware.IsSuperAdmin(c) {
        return c.Status(403).JSON(fiber.Map{
            "error": "Super admin only",
        })
    }
    
    var articles []entity.Articles
    // No client filtering for super admin
    db.Find(&articles)
    
    return c.JSON(articles)
}

Use Case 3: User Switching Between Clients

// User bisa switch antara clients yang dia punya akses
func (ctrl *UserController) SwitchClient(c *fiber.Ctx) error {
    targetClientId := c.Params("client_id")
    clientUUID, _ := uuid.Parse(targetClientId)
    
    userId := c.Locals(customMiddleware.UserIDContextKey).(uint)
    
    // Verify user has access
    hasAccess, _ := clientUtils.HasAccessToClient(
        db, 
        userId, 
        clientUUID, 
        false,
    )
    
    if !hasAccess {
        return c.Status(403).JSON(fiber.Map{
            "error": "No access to this client",
        })
    }
    
    // Update user's primary client or set in session
    // Return success
}

🔒 Security Considerations

  1. Always validate client access sebelum menampilkan/memodifikasi data
  2. Super admin flag harus di-protect dengan ketat (database constraint)
  3. Audit trail untuk perubahan UserClientAccess
  4. Cascade rules untuk delete parent client
  5. Rate limiting untuk multi-client queries

🐛 Troubleshooting

Issue: User tidak bisa akses client

// Debug: Check accessible clients
accessibleClients, _ := clientUtils.GetAccessibleClientIDs(db, userId, false)
fmt.Println("Accessible clients:", accessibleClients)

Issue: Sub-clients tidak muncul

// Pastikan IncludeSubClients = true
db.Model(&entity.UserClientAccess{}).
    Where("user_id = ?", userId).
    Update("include_sub_clients", true)

Issue: Super admin tidak bisa akses

// Verify super admin flag
var user entity.Users
db.First(&user, userId)
fmt.Println("Is Super Admin:", *user.IsSuperAdmin)

📊 Performance Tips

  1. Index pada parent_client_id dan user_id, client_id (sudah ada di entity)
  2. Cache accessible client IDs per user (Redis recommended)
  3. Batch queries untuk sub-client retrieval
  4. Pagination untuk multi-client data

🔄 Backward Compatibility

Kode lama tetap berfungsi:

  • middleware.GetClientID(c) masih tersedia
  • ClientId di Users tetap ada sebagai primary client
  • Single-client filtering tetap work
  • Header X-Client-Key masih didukung

Migration ke V2 bisa dilakukan bertahap per module.


📞 Support

Untuk pertanyaan atau issue, silakan hubungi tim development.