-
Notifications
You must be signed in to change notification settings - Fork 0
Integrations Design
Date of Discussion: October 3, 2024
We explored designing a reusable and scalable integrations feature for the FrontDesk application, accommodating multi-tenancy and the ability for users to be linked to multiple organizations. The main goals are:
- Multi-Tenancy Support: Users can belong to multiple organizations (tenants), and each organization manages its own properties and integrations.
- User Roles and Permissions: Different users may have different roles within organizations, affecting their access to properties and integrations.
- Flexible Integrations: The system should allow organizations to connect to various OTAs (Online Travel Agencies) like Hospitable, Airbnb, and others in a reusable and generic manner.
- C# Implementation: The backend will be implemented in C#.
- Organization: Represents a tenant in the system.
- User: Can belong to multiple organizations with specific roles.
- Property: Belongs to an organization.
- Integration: Represents an OTA or third-party service.
- Connection: Represents an organization's connection to an integration.
- Account: Stores organization-specific account details for an integration.
- Role-Based Access Control (RBAC): Manages user permissions within organizations.
public class Organization
{
public Guid OrganizationId { get; set; }
public string Name { get; set; }
public ICollection<OrganizationUser> OrganizationUsers { get; set; }
public ICollection<Property> Properties { get; set; }
public ICollection<Connection> Connections { get; set; }
}public class User
{
public Guid UserId { get; set; }
public string Email { get; set; }
public ICollection<OrganizationUser> OrganizationUsers { get; set; }
}public class OrganizationUser
{
public Guid OrganizationId { get; set; }
public Organization Organization { get; set; }
public Guid UserId { get; set; }
public User User { get; set; }
public string Role { get; set; } // e.g., "Owner", "Manager", "Viewer"
}public class Property
{
public Guid PropertyId { get; set; }
public string Name { get; set; }
public Guid OrganizationId { get; set; }
public Organization Organization { get; set; }
// Other property-specific fields
}public class Integration
{
public Guid IntegrationId { get; set; }
public string Name { get; set; } // e.g., "Hospitable", "Airbnb"
public string ProviderKey { get; set; } // e.g., "hospitable", "airbnb"
public string LogoUrl { get; set; }
public bool IsActive { get; set; }
}Represents an organization's connection to an integration.
public class Connection
{
public Guid ConnectionId { get; set; }
public Guid OrganizationId { get; set; }
public Organization Organization { get; set; }
public Guid IntegrationId { get; set; }
public Integration Integration { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime TokenExpiresAt { get; set; }
public string Status { get; set; } // e.g., "Connected", "Disconnected"
public ICollection<Account> Accounts { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}Stores integration-specific account details for an organization.
public class Account
{
public Guid AccountId { get; set; }
public Guid ConnectionId { get; set; }
public Connection Connection { get; set; }
public string ExternalAccountId { get; set; }
public string AccountName { get; set; }
public string Email { get; set; }
public string MetadataJson { get; set; } // JSON string for additional data
}Implement RBAC to manage user permissions within organizations.
- Owner: Full access to all organization resources, including integrations.
- Manager: Can manage properties and integrations but may have some restrictions.
- Viewer: Read-only access to properties and bookings.
Implement methods to check if a user has the necessary permissions to perform actions.
public class AuthorizationService
{
public bool CanManageIntegrations(Guid userId, Guid organizationId)
{
// Fetch the OrganizationUser record
var orgUser = _dbContext.OrganizationUsers
.FirstOrDefault(ou => ou.UserId == userId && ou.OrganizationId == organizationId);
if (orgUser == null)
return false;
return orgUser.Role == "Owner" || orgUser.Role == "Manager";
}
// Other permission checks...
}Use C# interfaces and abstract classes to define a common contract for all integrations.
public interface IIntegration
{
Task AuthenticateAsync(Guid organizationId, AuthenticationParameters parameters);
Task RefreshTokenAsync(Guid connectionId);
Task<Account> GetAccountDetailsAsync(Guid connectionId);
Task DisconnectAsync(Guid connectionId);
}public class AuthenticationParameters
{
// Parameters required for authentication
public string AuthorizationCode { get; set; }
public string RedirectUri { get; set; }
public Dictionary<string, string> AdditionalData { get; set; }
}public class HospitableIntegration : IIntegration
{
public async Task AuthenticateAsync(Guid organizationId, AuthenticationParameters parameters)
{
// Hospitable-specific authentication logic
}
public async Task RefreshTokenAsync(Guid connectionId)
{
// Hospitable-specific token refresh logic
}
public async Task<Account> GetAccountDetailsAsync(Guid connectionId)
{
// Fetch account details from Hospitable API
}
public async Task DisconnectAsync(Guid connectionId)
{
// Hospitable-specific disconnect logic
}
}public class AirbnbIntegration : IIntegration
{
public async Task AuthenticateAsync(Guid organizationId, AuthenticationParameters parameters)
{
// Airbnb-specific authentication logic
}
public async Task RefreshTokenAsync(Guid connectionId)
{
// Airbnb-specific token refresh logic
}
public async Task<Account> GetAccountDetailsAsync(Guid connectionId)
{
// Fetch account details from Airbnb API
}
public async Task DisconnectAsync(Guid connectionId)
{
// Airbnb-specific disconnect logic
}
}Implement a factory to create integration instances based on the provider key.
public static class IntegrationFactory
{
public static IIntegration CreateIntegration(string providerKey)
{
switch (providerKey.ToLower())
{
case "hospitable":
return new HospitableIntegration();
case "airbnb":
return new AirbnbIntegration();
// Add cases for other integrations
default:
throw new NotSupportedException($"Provider '{providerKey}' is not supported.");
}
}
}For integrations using OAuth 2.0 (e.g., Hospitable):
-
Initiate OAuth Flow:
- Redirect the user to the OTA's authorization URL with necessary query parameters.
-
Handle OAuth Callback:
- Create an endpoint to handle the OTA's redirect with an authorization code.
- Use the code to request access and refresh tokens.
-
Store Tokens:
- Save the tokens in the Connection model associated with the organization.
For integrations using API keys:
-
API Key Input:
- Provide a secure form for the user to enter the API key.
-
Validate API Key:
- Optionally validate the API key by making a test API call.
-
Store API Key:
- Save the API key in the Connection model.
public async Task AuthenticateIntegrationAsync(Guid organizationId, string providerKey, AuthenticationParameters parameters)
{
var integration = IntegrationFactory.CreateIntegration(providerKey);
// Check if the user has permission
if (!_authorizationService.CanManageIntegrations(_currentUserId, organizationId))
throw new UnauthorizedAccessException("You do not have permission to manage integrations for this organization.");
await integration.AuthenticateAsync(organizationId, parameters);
}- Encryption: Use encryption mechanisms like Data Protection API (DPAPI) or Azure Key Vault for storing sensitive data.
- Hashing: Do not hash tokens; they need to be retrieved in plaintext for API calls.
- Secure Storage: Store secrets securely in your configuration files or environment variables, not in source code.
public class SecureTokenService
{
private readonly IDataProtector _protector;
public SecureTokenService(IDataProtectionProvider dataProtectionProvider)
{
_protector = dataProtectionProvider.CreateProtector("SecureTokenProtector");
}
public string Protect(string plaintext)
{
return _protector.Protect(plaintext);
}
public string Unprotect(string protectedText)
{
return _protector.Unprotect(protectedText);
}
}- Organization Context: Display integrations within the context of the selected organization.
- List Available Integrations: Show all supported OTAs with connection status for the organization.
- Action Buttons: Allow users to connect, reconnect, or disconnect integrations based on their permissions.
- Provide a mechanism for users to switch between organizations they belong to.
- Disable or hide integration management options for users without sufficient permissions.
- Graceful Errors: Catch exceptions and provide user-friendly error messages.
- Logging: Use logging frameworks like Serilog or NLog for error logging.
- Notifications: Inform users of successful connections, errors, or required actions (e.g., re-authentication).
-
Property Owner:
- Has an organization representing their properties.
- Invites the management company as users with appropriate roles (e.g., "Manager").
-
Management Company:
- Users can switch between different organizations (property owners) they manage.
- Access is governed by their role within each organization.
-
The management company can:
- Switch between organizations (clients) they have access to.
- Manage properties and integrations within each organization, depending on their permissions.
- Use the AuthorizationService to enforce permissions on actions like connecting integrations, managing properties, etc.
- Input Validation: Validate all user inputs to prevent injection attacks.
- HTTPS Enforcement: Ensure all communications are over HTTPS.
- Token Expiry Handling: Implement logic to refresh tokens before they expire.
- Audit Trails: Keep logs of user actions related to integrations for auditing purposes.
- Unit Tests: Write unit tests for each component, mocking external dependencies.
- Integration Tests: Test the end-to-end flow with sandbox environments or mocks.
- Permission Tests: Verify that users cannot perform actions beyond their permissions.
- Implement the Updated Models: Update your database schema and data models to reflect the multi-tenancy design.
- Develop the Integration Interfaces: Code the interfaces and OTA-specific implementations.
- Set Up Authentication Flows: Configure OAuth 2.0 and API key authentication mechanisms.
- Build the User Interface: Design the UI to handle organization switching and integration management.
- Enforce Permissions: Implement authorization checks throughout your application.
- Test Thoroughly: Perform unit, integration, and permission tests.
- Gather User Feedback: Pilot the feature with a subset of users and iterate based on feedback.