10 KiB
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
- Standalone Client - Client mandiri tanpa parent/child
- Parent Client - Client yang memiliki sub-clients
- Sub Client - Client yang berada di bawah parent client
User Types
- Super Admin - Platform administrator, akses ke SEMUA clients
- Multi-Client User - User dengan akses ke beberapa clients (misal: manager regional)
- 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
Option A: Gunakan Middleware V2 (Recommended)
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
- Always validate client access sebelum menampilkan/memodifikasi data
- Super admin flag harus di-protect dengan ketat (database constraint)
- Audit trail untuk perubahan UserClientAccess
- Cascade rules untuk delete parent client
- 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
- Index pada
parent_client_iddanuser_id, client_id(sudah ada di entity) - Cache accessible client IDs per user (Redis recommended)
- Batch queries untuk sub-client retrieval
- Pagination untuk multi-client data
🔄 Backward Compatibility
Kode lama tetap berfungsi:
- ✅
middleware.GetClientID(c)masih tersedia - ✅
ClientIddi Users tetap ada sebagai primary client - ✅ Single-client filtering tetap work
- ✅ Header
X-Client-Keymasih didukung
Migration ke V2 bisa dilakukan bertahap per module.
📞 Support
Untuk pertanyaan atau issue, silakan hubungi tim development.