# Multi-Tenancy Implementation Guide

## Overview

This implementation provides **Single Database Multi-Tenancy** for the CBT application. Each LBB (Lembaga Bimbingan Belajar) has its own isolated data within the same database, accessed via subdomain routing.

## Architecture

### Key Components

1. **Middleware**: `SetLbbContextFromSubdomain` - Identifies LBB from subdomain
2. **Global Scope**: `LbbScope` - Automatically filters queries by `lbb_id`
3. **Tenant Models**: Models with `lbb_id` column have automatic data isolation
4. **Subdomain Routing**: Access LBBs via subdomain (e.g., `abc.domain.com`)

### How It Works

```
1. User logs in at central domain (domain.com)
2. User selects LBB from list of accessible LBBs
3. Redirects to subdomain (e.g., abc.domain.com)
4. Middleware extracts subdomain → finds LBB → sets session
5. Global scope automatically filters all tenant model queries by lbb_id
6. User sees only their LBB's data
```

## Models with Data Isolation

The following models have automatic `lbb_id` filtering:

- ✅ **Student** - Student records
- ✅ **ClassModel** - Classes
- ✅ **Exam** - Exams
- ✅ **Question** - Questions
- ✅ **LbbSetting** - LBB settings (branding)
- ✅ **TokenTransaction** - Token transactions
- ✅ **Commission** - Commissions
- ✅ **TokenOrder** - Token orders

## Models without Data Isolation (Central Only)

- **User** - Global users (can belong to multiple LBBs)
- **Lbb** - Central LBB management
- **Sales** - Sales management
- **Setting** - Application-wide settings
- **Withdraw** - Withdrawal requests
- **BankAccount** - Bank accounts
- Other central tables...

## User Roles & Access

### Super Admin
- **Access**: All LBBs via central domain
- **Purpose**: Manage all LBBs, users, system settings
- **Subdomain**: Can access any LBB subdomain

### Sales
- **Access**: Assigned LBBs via central domain
- **Purpose**: Manage token sales, commissions
- **Subdomain**: Can only access assigned LBB subdomain(s)
- **Restriction**: `user.lbb_id` must match current LBB

### Admin (LBB Admin)
- **Access**: Their assigned LBB only
- **Purpose**: Manage students, exams, questions
- **Subdomain**: Can only access their LBB subdomain
- **Restriction**: `user.lbb_id` must match current LBB

### Student (Siswa)
- **Access**: Multiple LBBs where they have student records
- **Purpose**: Take exams, view results
- **Subdomain**: Can access any LBB where they're registered
- **Restriction**: Must have `Student` record in current LBB

## Middleware: SetLbbContextFromSubdomain

### Purpose
Extracts subdomain from request, finds LBB, validates access, and sets session.

### Flow

1. **Extract Subdomain**: Parse host to get subdomain (e.g., `abc` from `abc.domain.com`)
2. **Skip Central Domains**: Continue without LBB context for central domains
3. **Find LBB**: Query `lbbs` table by `subdomain`
4. **Validate Access**: Check if user has access to this LBB
5. **Set Session**: Store `current_lbb_id` in session

### Central Domains

By default, these are considered central:
- `localhost`
- `127.0.0.1`
- Value from `APP_DOMAIN` env variable

### Access Validation

```php
// Super Admin: Access to all LBBs
if ($user->role === UserRole::SUPER_ADMIN) return true;

// Sales: Access to assigned LBBs
if ($user->role === UserRole::SALES) 
    return $user->lbb_id === $lbb->id;

// Admin: Access to own LBB
if ($user->role === UserRole::ADMIN) 
    return $user->lbb_id === $lbb->id;

// Student: Access to LBBs where registered
if ($user->role === UserRole::SISWA) {
    $record = Student::where('user_id', $user->id)
        ->where('lbb_id', $lbb->id)
        ->first();
    return $record != null;
}
```

## Global Scope: LbbScope

### Purpose
Automatically adds `WHERE lbb_id = X` to all queries for tenant models.

### Implementation

```php
protected function apply(Builder $builder, Model $model)
{
    $currentLbbId = session('current_lbb_id');
    
    if (!$currentLbbId) {
        return; // No scope applied on central domain
    }
    
    $builder->where($model->getTable() . '.lbb_id', $currentLbbId);
}
```

### Effect

All these queries are automatically filtered:

```php
// On subdomain (with LBB context)
$students = Student::all(); 
// Executes: SELECT * FROM students WHERE lbb_id = 1

$exams = Exam::where('status', 'active')->get();
// Executes: SELECT * FROM exams WHERE status = 'active' AND lbb_id = 1

// On central domain (no LBB context)
$students = Student::all(); 
// Executes: SELECT * FROM students (no lbb_id filter)
```

### Disabling Scope (If Needed)

```php
// Get all students across all LBBs (only from central domain)
$allStudents = Student::withoutGlobalScopes()->get();

// Get specific student by ID
$student = Student::withoutGlobalScopes()->find($id);
```

## Routing Strategy

### Central Domain Routes (`domain.com`)

Located in `routes/web.php`:
- Login, register, logout
- LBB selection page
- Super admin dashboard
- Sales dashboard
- Central operations

### Subdomain Routes (`abc.domain.com`)

Same routes as central domain, but with LBB context:
- Admin LBB dashboard
- Student dashboard
- Exam management
- Question management
- All tenant-specific operations

### Example Usage

```php
// routes/web.php

// Central routes (no middleware for LBB context)
Route::get('/select-lbb', [LbbSelectionController::class, 'index'])->name('lbb.select');
Route::post('/select-lbb/{lbb_id}', [LbbSelectionController::class, 'select'])->name('lbb.select.store');

// Subdomain routes (with LBB context middleware)
Route::middleware(['web', 'auth', SetLbbContextFromSubdomain::class])
    ->group(function () {
        Route::get('/admin/students', [StudentController::class, 'index'])->name('admin.students.index');
        Route::get('/admin/exams', [ExamController::class, 'index'])->name('admin.exams.index');
        Route::get('/student/dashboard', [StudentDashboardController::class, 'index'])->name('student.dashboard');
    });
```

## LBB Selection Flow

### 1. After Login

User redirected to LBB selection page (`/select-lbb`):

```php
public function index()
{
    $user = auth()->user();
    
    switch ($user->role) {
        case UserRole::SUPER_ADMIN:
            $lbbs = Lbb::all();
            break;
        case UserRole::SALES:
            $lbbs = Lbb::where('id', $user->lbb_id)->get();
            break;
        case UserRole::ADMIN:
            $lbbs = Lbb::where('id', $user->lbb_id)->get();
            break;
        case UserRole::SISWA:
            $lbbs = Lbb::whereIn('id', 
                Student::where('user_id', $user->id)->pluck('lbb_id')
            )->get();
            break;
    }
    
    return view('lbb-selection', compact('lbbs'));
}
```

### 2. User Selects LBB

```php
public function select(Request $request, $lbbId)
{
    $user = auth()->user();
    $lbb = Lbb::findOrFail($lbbId);
    
    // Validate access
    if (!$this->userHasAccessToLbb($user, $lbb)) {
        return back()->with('error', 'Anda tidak memiliki akses ke LBB ini.');
    }
    
    // Set session
    session(['current_lbb_id' => $lbb->id]);
    
    // Redirect to subdomain
    $subdomain = $lbb->subdomain;
    $baseDomain = str_replace('www.', '', request()->getHttpHost());
    $baseUrl = "https://{$subdomain}.{$baseDomain}";
    
    return redirect()->away($baseUrl);
}
```

## Branding per LBB

### Using LbbSettings

```php
// Get current LBB settings
$lbbId = session('current_lbb_id');
$lbbSettings = LbbSetting::where('lbb_id', $lbbId)->first();

// In views
<style>
    :root {
        --primary-color: {{ $lbbSettings->theme_primary_color ?? '#3B82F6' }};
        --secondary-color: {{ $lbbSettings->theme_secondary_color ?? '#1E40AF' }};
        --accent-color: {{ $lbbSettings->theme_accent_color ?? '#F59E0B' }};
        --font-color: {{ $lbbSettings->theme_font_color ?? '#1F2937' }};
    }
</style>

// Logo
<img src="{{ asset($lbbSettings->logo_path) }}" alt="{{ $lbbSettings->display_name }}">
```

### Global Sharing via Middleware

Create `app/Http/Middleware/ShareLbbSettings.php`:

```php
public function handle(Request $request, Closure $next)
{
    if (session('current_lbb_id')) {
        $lbbSettings = LbbSetting::where('lbb_id', session('current_lbb_id'))->first();
        view()->share('lbbSettings', $lbbSettings);
    }
    
    return $next($request);
}
```

## File Storage (Optional Isolation)

If you need file isolation per LBB:

```php
// Upload path
$path = request()->file('avatar')->store("lbb/{$lbbId}/avatars", 'public');

// Access
$url = Storage::disk('public')->url("lbb/{$lbbId}/avatars/filename.jpg");
```

## Data Isolation Examples

### Creating Data

```php
// On subdomain with LBB context
Student::create([
    'user_id' => $userId,
    'class_id' => $classId,
    'lbb_id' => $lbbId, // Must be set!
    'status' => 'active',
]);
```

**Important**: When creating data, always explicitly set `lbb_id`:

```php
$lbbId = session('current_lbb_id');

ClassModel::create([
    'name' => 'Kelas 10',
    'lbb_id' => $lbbId, // CRITICAL: Set explicitly
]);
```

### Updating Data

```php
// Automatically filtered by lbb_id
$student = Student::find($id); // Finds only in current LBB
$student->update(['status' => 'inactive']);
```

### Deleting Data

```php
// Automatically filtered by lbb_id
$student = Student::find($id); // Finds only in current LBB
$student->delete();
```

### Querying Relationships

```php
// Get students with their class (both filtered by lbb_id)
$students = Student::with('classModel')->get();

// Get exams with questions (both filtered by lbb_id)
$exams = Exam::with('questions')->get();
```

## Best Practices

### 1. Always Set lbb_id When Creating

```php
// ❌ WRONG (lbb_id will be null)
Student::create(['name' => 'John']);

// ✅ CORRECT
Student::create([
    'name' => 'John',
    'lbb_id' => session('current_lbb_id'),
]);
```

### 2. Use Relationships Instead of Manual Joins

```php
// ❌ WRONG (might bypass scope)
$students = DB::table('students')
    ->join('classes', 'students.class_id', '=', 'classes.id')
    ->get();

// ✅ CORRECT
$students = Student::with('classModel')->get();
```

### 3. Validate lbb_id in Forms

```php
// In form requests
public function rules()
{
    return [
        'lbb_id' => 'required|exists:lbbs,id',
        'name' => 'required|string',
    ];
}

// In controller
$lbbId = session('current_lbb_id');
$validated['lbb_id'] = $lbbId; // Override with session value
```

### 4. Check LBB Context Before Operations

```php
public function someOperation()
{
    if (!session('current_lbb_id')) {
        return back()->with('error', 'Please select an LBB first.');
    }
    
    // Proceed with operation
}
```

## Testing Locally

### Option 1: Edit /etc/hosts (Mac/Linux)

```bash
sudo nano /etc/hosts
```

Add:
```
127.0.0.1       localhost
127.0.0.1       abc.localhost
127.0.0.1       xyz.localhost
```

Access:
- `http://localhost` - Central domain
- `http://abc.localhost` - LBB with subdomain "abc"
- `http://xyz.localhost` - LBB with subdomain "xyz"

### Option 2: Use Laravel Valet

```bash
valet link cbtq
```

Access:
- `http://cbtq.test` - Central domain
- `http://abc.cbtq.test` - LBB with subdomain "abc"

### Option 3: Use ngrok (for testing on mobile)

```bash
ngrok http 8000
```

Use the provided URL, then access subdomains via subdomain.ngrok.io

## Troubleshooting

### Issue: Query returns empty results

**Cause**: `current_lbb_id` not set in session
**Solution**: Check middleware is running and LBB is found

### Issue: Can't access data from other LBBs

**Cause**: Global scope is filtering by `lbb_id`
**Solution**: Use `withoutGlobalScopes()` if needed (only from central domain)

### Issue: Student can't see other LBBs

**Cause**: Student doesn't have `Student` record in other LBB
**Solution**: Create `Student` record for each LBB user belongs to

### Issue: Subdomain not recognized

**Cause**: Subdomain extraction logic or DNS issue
**Solution**: Check `/etc/hosts` or DNS configuration

### Issue: Data appears in wrong LBB

**Cause**: Not setting `lbb_id` explicitly when creating records
**Solution**: Always set `lbb_id = session('current_lbb_id')`

## Security Considerations

### 1. Middleware Protection

Always use `SetLbbContextFromSubdomain` middleware on subdomain routes.

### 2. Access Validation

The middleware validates user access before setting LBB context.

### 3. SQL Injection Prevention

Global scopes protect against `lbb_id` manipulation.

### 4. Session Hijacking

Use HTTPS in production to protect session data.

## Performance Considerations

### 1. Index lbb_id

Ensure all tenant tables have indexes on `lbb_id`:

```sql
CREATE INDEX idx_lbb_id ON students(lbb_id);
CREATE INDEX idx_lbb_id ON exams(lbb_id);
-- etc.
```

### 2. Cache LBB Settings

Cache LBB settings to avoid repeated queries:

```php
$lbbSettings = Cache::remember(
    "lbb_settings_{$lbbId}",
    3600,
    fn() => LbbSetting::where('lbb_id', $lbbId)->first()
);
```

### 3. Optimize Queries

Use eager loading to avoid N+1 queries:

```php
// ❌ N+1 queries
$students = Student::all();
foreach ($students as $student) {
    echo $student->classModel->name; // Separate query each time
}

// ✅ Optimized
$students = Student::with('classModel')->get();
foreach ($students as $student) {
    echo $student->classModel->name; // No extra queries
}
```

## Migration Guide

### From Single Tenant to Multi-Tenant

1. **Add lbb_id column** to all tenant tables
2. **Backfill data** with default LBB ID
3. **Add foreign keys** to `lbbs` table
4. **Apply global scopes** to models
5. **Update controllers** to set `lbb_id` when creating data
6. **Register middleware** in routes
7. **Test thoroughly**

### Example Migration

```php
public function up()
{
    Schema::table('students', function (Blueprint $table) {
        $table->foreignId('lbb_id')->nullable()->constrained('lbbs');
    });
    
    // Backfill with first LBB
    $defaultLbbId = DB::table('lbbs')->first()->id;
    DB::table('students')->update(['lbb_id' => $defaultLbbId]);
    
    Schema::table('students', function (Blueprint $table) {
        $table->foreignId('lbb_id')->nullable(false)->change();
    });
}
```

## Summary

This multi-tenancy implementation provides:

✅ **Data Isolation**: Each LBB sees only their own data
✅ **Subdomain Access**: Branding via subdomain routing
✅ **Role-Based Access**: Different access levels per user role
✅ **Automatic Filtering**: Global scopes handle `lbb_id` automatically
✅ **Flexible**: Easy to add new tenant models
✅ **Secure**: Middleware validates access before setting context
✅ **Simple**: Single database, no complex setup

The system is production-ready and can scale to handle hundreds of LBBs on a single database.