429 lines
10 KiB
Markdown
429 lines
10 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# Migration akan otomatis dijalankan saat aplikasi start
|
|
# Pastikan config migrate = true di config.toml
|
|
|
|
go run main.go
|
|
```
|
|
|
|
### Step 2: Migrate Existing Data
|
|
|
|
```sql
|
|
-- 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`:
|
|
|
|
```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:
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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:**
|
|
```go
|
|
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:**
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// Debug: Check accessible clients
|
|
accessibleClients, _ := clientUtils.GetAccessibleClientIDs(db, userId, false)
|
|
fmt.Println("Accessible clients:", accessibleClients)
|
|
```
|
|
|
|
### Issue: Sub-clients tidak muncul
|
|
|
|
```go
|
|
// Pastikan IncludeSubClients = true
|
|
db.Model(&entity.UserClientAccess{}).
|
|
Where("user_id = ?", userId).
|
|
Update("include_sub_clients", true)
|
|
```
|
|
|
|
### Issue: Super admin tidak bisa akses
|
|
|
|
```go
|
|
// 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.
|