5.9 KiB
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
-
Data Isolation:
- Always use tenant-specific storage
- Clear tenant data on switch
- Validate tenant context
-
Security:
- Always include tenant headers
- Validate tenant permissions
- Handle unauthorized access
-
Performance:
- Cache tenant-specific data
- Optimize tenant switching
- Handle offline scenarios
-
User Experience:
- Show tenant context
- Handle tenant switching gracefully
- Provide clear feedback
🔄 Implementation Checklist
When implementing tenant-specific features:
- Add tenant headers to requests
- Implement tenant storage
- Add feature flags
- Implement tenant switching
- Add proper validation
- Handle errors appropriately
- Test all scenarios
📚 Additional Resources
- Backend Multi-tenancy:
/docs/mcp/MULTI_TENANCY.md - Frontend Architecture:
/docs/ARCHITECTURE.md - Authentication Guide:
/docs/AUTHENTICATION.md