testes/Modulos Angular/projects/idt_app/docs/backend-integration/MULTI_TENANCY.md

5.9 KiB

🏢 Multi-tenancy Implementation - PraFrota Frontend

Overview

This document outlines how the PraFrota frontend implements multi-tenancy to align with our backend architecture, ensuring proper tenant isolation and data management.

🏗️ Tenant Context

Tenant Service

@Injectable({
  providedIn: 'root'
})
export class TenantService {
  private currentTenant = new BehaviorSubject<Tenant | null>(null);
  
  constructor(private http: HttpClient) {}

  // Get current tenant
  getCurrentTenant(): Observable<Tenant | null> {
    return this.currentTenant.asObservable();
  }

  // Set current tenant
  setCurrentTenant(tenant: Tenant): void {
    this.currentTenant.next(tenant);
  }

  // Load tenant data
  loadTenantData(tenantId: string): Observable<Tenant> {
    return this.http.get<Response<Tenant>>(`/api/tenant/${tenantId}`).pipe(
      map(response => response.data),
      tap(tenant => this.setCurrentTenant(tenant))
    );
  }
}

Tenant Interface

interface Tenant {
  id: string;
  name: string;
  settings: TenantSettings;
  features: string[];
  permissions: string[];
}

🔐 Authentication & Authorization

Auth Interceptor

@Injectable()
export class TenantAuthInterceptor implements HttpInterceptor {
  constructor(private tenantService: TenantService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.tenantService.getCurrentTenant().pipe(
      take(1),
      switchMap(tenant => {
        if (!tenant) {
          return next.handle(req);
        }

        const modifiedReq = req.clone({
          setHeaders: {
            'tenant-uuid': tenant.id,
            'tenant-user-auth': this.getAuthToken()
          }
        });

        return next.handle(modifiedReq);
      })
    );
  }

  private getAuthToken(): string {
    // Get token from secure storage
    return localStorage.getItem('auth_token') || '';
  }
}

📦 Data Isolation

Tenant-specific Storage

@Injectable({
  providedIn: 'root'
})
export class TenantStorageService {
  private readonly TENANT_PREFIX = 'tenant_';

  constructor(private tenantService: TenantService) {}

  // Store tenant-specific data
  setItem(key: string, value: any): void {
    this.tenantService.getCurrentTenant().pipe(
      take(1)
    ).subscribe(tenant => {
      if (tenant) {
        const tenantKey = `${this.TENANT_PREFIX}${tenant.id}_${key}`;
        localStorage.setItem(tenantKey, JSON.stringify(value));
      }
    });
  }

  // Get tenant-specific data
  getItem<T>(key: string): Observable<T | null> {
    return this.tenantService.getCurrentTenant().pipe(
      take(1),
      map(tenant => {
        if (!tenant) return null;
        const tenantKey = `${this.TENANT_PREFIX}${tenant.id}_${key}`;
        const value = localStorage.getItem(tenantKey);
        return value ? JSON.parse(value) : null;
      })
    );
  }
}

🔄 Tenant Switching

Tenant Switch Component

@Component({
  selector: 'app-tenant-switch',
  standalone: true,
  template: `
    <div class="tenant-switch">
      <select [formControl]="tenantControl" (change)="onTenantChange()">
        <option *ngFor="let tenant of tenants" [value]="tenant.id">
          {{tenant.name}}
        </option>
      </select>
    </div>
  `
})
export class TenantSwitchComponent {
  tenantControl = new FormControl('');
  tenants: Tenant[] = [];

  constructor(
    private tenantService: TenantService,
    private router: Router
  ) {}

  onTenantChange(): void {
    const tenantId = this.tenantControl.value;
    if (tenantId) {
      this.tenantService.loadTenantData(tenantId).subscribe(() => {
        // Clear tenant-specific data
        this.clearTenantData();
        // Reload current route
        this.router.navigateByUrl(this.router.url);
      });
    }
  }

  private clearTenantData(): void {
    // Clear tenant-specific data from storage
    // Clear tenant-specific state
    // Clear tenant-specific cache
  }
}

📊 Tenant-specific Features

Feature Flags

@Injectable({
  providedIn: 'root'
})
export class FeatureFlagService {
  constructor(private tenantService: TenantService) {}

  isFeatureEnabled(feature: string): Observable<boolean> {
    return this.tenantService.getCurrentTenant().pipe(
      map(tenant => tenant?.features.includes(feature) ?? false)
    );
  }
}

Feature Guard

@Injectable({
  providedIn: 'root'
})
export class FeatureGuard implements CanActivate {
  constructor(
    private featureFlagService: FeatureFlagService,
    private router: Router
  ) {}

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    const requiredFeature = route.data['requiredFeature'];
    return this.featureFlagService.isFeatureEnabled(requiredFeature).pipe(
      tap(enabled => {
        if (!enabled) {
          this.router.navigate(['/unauthorized']);
        }
      })
    );
  }
}

📝 Best Practices

  1. Data Isolation:

    • Always use tenant-specific storage
    • Clear tenant data on switch
    • Validate tenant context
  2. Security:

    • Always include tenant headers
    • Validate tenant permissions
    • Handle unauthorized access
  3. Performance:

    • Cache tenant-specific data
    • Optimize tenant switching
    • Handle offline scenarios
  4. User Experience:

    • Show tenant context
    • Handle tenant switching gracefully
    • Provide clear feedback

🔄 Implementation Checklist

When implementing tenant-specific features:

  1. Add tenant headers to requests
  2. Implement tenant storage
  3. Add feature flags
  4. Implement tenant switching
  5. Add proper validation
  6. Handle errors appropriately
  7. Test all scenarios

📚 Additional Resources

  • Backend Multi-tenancy: /docs/mcp/MULTI_TENANCY.md
  • Frontend Architecture: /docs/ARCHITECTURE.md
  • Authentication Guide: /docs/AUTHENTICATION.md