@frank-auth/react
Version:
Flexible and customizable React UI components for Frank Authentication
852 lines (773 loc) • 27.2 kB
text/typescript
/**
* @frank-auth/react - Organization Configuration
*
* Organization-specific configuration management with support for
* multi-tenant settings, branding, and feature customization.
*/
import type {
AppearanceConfig,
FrankAuthUIConfig,
LocalizationConfig,
OrganizationConfig,
Theme,
UserType,
} from './types';
import {DEFAULT_ORGANIZATION_CONFIG} from './defaults';
import {ThemeManager} from './theme';
import {AppearanceManager} from './appearance';
// ============================================================================
// Organization-Specific Types
// ============================================================================
/**
* Organization feature flags with granular control
*/
export interface OrganizationFeatureFlags {
// Authentication features
authentication: {
signUp: boolean;
signIn: boolean;
passwordReset: boolean;
emailVerification: boolean;
phoneVerification: boolean;
socialAuth: boolean;
magicLink: boolean;
passkeys: boolean;
};
// Security features
security: {
mfa: boolean;
mfaRequired: boolean;
sso: boolean;
sessionManagement: boolean;
auditLogs: boolean;
ipWhitelist: boolean;
deviceTrust: boolean;
riskAssessment: boolean;
};
// User management
userManagement: {
userProfiles: boolean;
userRoles: boolean;
userPermissions: boolean;
userInvitations: boolean;
userSuspension: boolean;
userDeletion: boolean;
bulkUserOperations: boolean;
};
// Organization features
organization: {
memberManagement: boolean;
roleManagement: boolean;
invitations: boolean;
customBranding: boolean;
customDomain: boolean;
webhooks: boolean;
apiAccess: boolean;
analytics: boolean;
};
// UI features
ui: {
darkMode: boolean;
customThemes: boolean;
localization: boolean;
customCSS: boolean;
logoUpload: boolean;
colorCustomization: boolean;
layoutCustomization: boolean;
};
// Integration features
integrations: {
saml: boolean;
oidc: boolean;
ldap: boolean;
scim: boolean;
slack: boolean;
microsoft: boolean;
google: boolean;
github: boolean;
};
}
/**
* Organization limits and quotas
*/
export interface OrganizationLimits {
users: {
maxUsers: number;
maxEndUsers: number;
maxExternalUsers: number;
maxInternalUsers: number;
};
sessions: {
maxSessionsPerUser: number;
maxConcurrentSessions: number;
sessionTimeout: number;
maxSessionDuration: number;
};
api: {
monthlyRequestLimit: number;
rateLimit: number;
burstLimit: number;
maxWebhooks: number;
};
storage: {
maxLogoSize: number;
maxCustomCSSSize: number;
auditLogRetention: number;
maxCustomFields: number;
};
features: {
maxRoles: number;
maxPermissions: number;
maxIntegrations: number;
maxDomains: number;
};
}
/**
* Organization compliance settings
*/
export interface OrganizationCompliance {
dataRetention: {
userDataRetention: number; // days
auditLogRetention: number; // days
sessionLogRetention: number; // days
automaticDeletion: boolean;
};
privacy: {
gdprCompliant: boolean;
ccpaCompliant: boolean;
hipaaCompliant: boolean;
soc2Compliant: boolean;
dataProcessingAgreement: boolean;
};
security: {
encryptionAtRest: boolean;
encryptionInTransit: boolean;
keyRotation: boolean;
backupEncryption: boolean;
accessLogging: boolean;
};
reporting: {
complianceReports: boolean;
auditReports: boolean;
securityReports: boolean;
dataExport: boolean;
rightToBeForgotten: boolean;
};
}
/**
* Extended organization configuration
*/
export interface ExtendedOrganizationConfig extends OrganizationConfig {
features: OrganizationFeatureFlags;
limits: OrganizationLimits;
compliance: OrganizationCompliance;
// Computed properties
tier: 'free' | 'starter' | 'professional' | 'enterprise';
isActive: boolean;
trialEndsAt?: Date;
subscriptionStatus: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid';
// Usage statistics
usage: {
currentUsers: number;
currentEndUsers: number;
monthlyApiRequests: number;
storageUsed: number;
lastActivityAt: Date;
};
}
// ============================================================================
// Organization Configuration Manager
// ============================================================================
export class OrganizationConfigManager {
private config: ExtendedOrganizationConfig;
private themeManager: ThemeManager;
private appearanceManager: AppearanceManager;
private listeners: Set<(config: ExtendedOrganizationConfig) => void> = new Set();
constructor(
organizationConfig: Partial<ExtendedOrganizationConfig>,
themeManager?: ThemeManager,
appearanceManager?: AppearanceManager
) {
this.config = this.mergeWithDefaults(organizationConfig);
this.themeManager = themeManager || new ThemeManager();
this.appearanceManager = appearanceManager || new AppearanceManager();
// Apply organization branding
this.applyOrganizationBranding();
}
/**
* Get current organization configuration
*/
getConfig(): ExtendedOrganizationConfig {
return { ...this.config };
}
/**
* Update organization configuration
*/
updateConfig(updates: Partial<ExtendedOrganizationConfig>): void {
this.config = {
...this.config,
...updates,
settings: { ...this.config.settings, ...updates.settings },
features: { ...this.config.features, ...updates.features },
limits: { ...this.config.limits, ...updates.limits },
compliance: { ...this.config.compliance, ...updates.compliance },
usage: { ...this.config.usage, ...updates.usage },
};
// Re-apply branding if branding settings changed
if (updates.settings?.branding) {
this.applyOrganizationBranding();
}
this.notifyListeners();
}
/**
* Check if a feature is enabled
*/
isFeatureEnabled(featurePath: string): boolean {
const keys = featurePath.split('.');
let current: any = this.config.features;
for (const key of keys) {
if (current?.[key] === undefined) {
return false;
}
current = current[key];
}
return Boolean(current);
}
/**
* Check if a user type is allowed
*/
isUserTypeAllowed(userType: UserType): boolean {
switch (userType) {
case 'internal':
return this.config.tier === 'enterprise';
case 'external':
return this.isFeatureEnabled('userManagement.userProfiles');
case 'end_user':
return true; // Always allowed
default:
return false;
}
}
/**
* Get user limits for a specific user type
*/
getUserLimits(userType: UserType): number {
switch (userType) {
case 'internal':
return this.config.limits.users.maxInternalUsers;
case 'external':
return this.config.limits.users.maxExternalUsers;
case 'end_user':
return this.config.limits.users.maxEndUsers;
default:
return 0;
}
}
/**
* Check if organization is within limits
*/
checkLimits(): {
withinLimits: boolean;
violations: Array<{ type: string; current: number; limit: number }>;
} {
const violations: Array<{ type: string; current: number; limit: number }> = [];
// Check user limits
if (this.config.usage.currentUsers > this.config.limits.users.maxUsers) {
violations.push({
type: 'users',
current: this.config.usage.currentUsers,
limit: this.config.limits.users.maxUsers,
});
}
if (this.config.usage.currentEndUsers > this.config.limits.users.maxEndUsers) {
violations.push({
type: 'endUsers',
current: this.config.usage.currentEndUsers,
limit: this.config.limits.users.maxEndUsers,
});
}
// Check API limits
if (this.config.usage.monthlyApiRequests > this.config.limits.api.monthlyRequestLimit) {
violations.push({
type: 'apiRequests',
current: this.config.usage.monthlyApiRequests,
limit: this.config.limits.api.monthlyRequestLimit,
});
}
return {
withinLimits: violations.length === 0,
violations,
};
}
/**
* Get organization tier configuration
*/
getTierConfig(): {
name: string;
features: string[];
limits: Record<string, number>;
price?: string;
} {
const tierConfigs = {
free: {
name: 'Free',
features: [
'Basic authentication',
'Up to 100 users',
'Email support',
],
limits: {
users: 100,
apiRequests: 1000,
sessions: 5,
},
},
starter: {
name: 'Starter',
price: '$29/month',
features: [
'Everything in Free',
'Up to 1,000 users',
'MFA support',
'Basic branding',
'Priority support',
],
limits: {
users: 1000,
apiRequests: 10000,
sessions: 10,
},
},
professional: {
name: 'Professional',
price: '$99/month',
features: [
'Everything in Starter',
'Up to 10,000 users',
'SSO integration',
'Advanced branding',
'API access',
'Webhooks',
'Audit logs',
],
limits: {
users: 10000,
apiRequests: 100000,
sessions: 25,
},
},
enterprise: {
name: 'Enterprise',
price: 'Custom',
features: [
'Everything in Professional',
'Unlimited users',
'SAML/LDAP integration',
'Custom domain',
'Advanced security',
'Compliance features',
'Dedicated support',
],
limits: {
users: -1, // Unlimited
apiRequests: -1, // Unlimited
sessions: -1, // Unlimited
},
},
};
return tierConfigs[this.config.tier];
}
/**
* Generate UI configuration based on organization settings
*/
generateUIConfig(): Partial<FrankAuthUIConfig> {
const baseConfig: Partial<FrankAuthUIConfig> = {
projectId: this.config.id,
organization: this.config,
features: {
signUp: this.isFeatureEnabled('authentication.signUp'),
signIn: this.isFeatureEnabled('authentication.signIn'),
passwordReset: this.isFeatureEnabled('authentication.passwordReset'),
mfa: this.isFeatureEnabled('security.mfa'),
sso: this.isFeatureEnabled('security.sso'),
organizationManagement: this.isFeatureEnabled('organization.memberManagement'),
userProfile: this.isFeatureEnabled('userManagement.userProfiles'),
sessionManagement: this.isFeatureEnabled('security.sessionManagement'),
},
};
// Apply theme customization if enabled
if (this.isFeatureEnabled('ui.customThemes')) {
baseConfig.theme = this.generateCustomTheme();
}
// Apply appearance customization if enabled
if (this.isFeatureEnabled('ui.customThemes')) {
baseConfig.appearance = this.generateCustomAppearance();
}
// Apply localization if enabled
if (this.isFeatureEnabled('ui.localization')) {
baseConfig.localization = this.generateLocalizationConfig();
}
return baseConfig;
}
/**
* Subscribe to configuration changes
*/
subscribe(callback: (config: ExtendedOrganizationConfig) => void): () => void {
this.listeners.add(callback);
return () => {
this.listeners.delete(callback);
};
}
/**
* Validate organization configuration
*/
validateConfig(): {
isValid: boolean;
errors: string[];
warnings: string[];
} {
const errors: string[] = [];
const warnings: string[] = [];
// Validate required fields
if (!this.config.id) {
errors.push('Organization ID is required');
}
if (!this.config.name) {
errors.push('Organization name is required');
}
// Validate limits
const { withinLimits, violations } = this.checkLimits();
if (!withinLimits) {
violations.forEach(violation => {
warnings.push(`${violation.type} limit exceeded: ${violation.current}/${violation.limit}`);
});
}
// Validate branding
if (this.config.settings.branding?.logo && !this.isValidUrl(this.config.settings.branding.logo)) {
errors.push('Invalid logo URL');
}
// Validate feature consistency
if (this.isFeatureEnabled('security.mfaRequired') && !this.isFeatureEnabled('security.mfa')) {
errors.push('MFA must be enabled if MFA is required');
}
if (this.isFeatureEnabled('organization.customDomain') && this.config.tier === 'free') {
warnings.push('Custom domain requires paid plan');
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
// Private methods
private mergeWithDefaults(config: Partial<ExtendedOrganizationConfig>): ExtendedOrganizationConfig {
const defaultFeatures: OrganizationFeatureFlags = {
authentication: {
signUp: true,
signIn: true,
passwordReset: true,
emailVerification: true,
phoneVerification: false,
socialAuth: false,
magicLink: false,
passkeys: false,
},
security: {
mfa: false,
mfaRequired: false,
sso: false,
sessionManagement: true,
auditLogs: false,
ipWhitelist: false,
deviceTrust: false,
riskAssessment: false,
},
userManagement: {
userProfiles: true,
userRoles: false,
userPermissions: false,
userInvitations: true,
userSuspension: false,
userDeletion: false,
bulkUserOperations: false,
},
organization: {
memberManagement: true,
roleManagement: false,
invitations: true,
customBranding: false,
customDomain: false,
webhooks: false,
apiAccess: false,
analytics: false,
},
ui: {
darkMode: true,
customThemes: false,
localization: true,
customCSS: false,
logoUpload: false,
colorCustomization: false,
layoutCustomization: false,
},
integrations: {
saml: false,
oidc: false,
ldap: false,
scim: false,
slack: false,
microsoft: false,
google: false,
github: false,
},
};
const defaultLimits: OrganizationLimits = {
users: {
maxUsers: 100,
maxEndUsers: 1000,
maxExternalUsers: 50,
maxInternalUsers: 5,
},
sessions: {
maxSessionsPerUser: 5,
maxConcurrentSessions: 100,
sessionTimeout: 1800, // 30 minutes
maxSessionDuration: 86400, // 24 hours
},
api: {
monthlyRequestLimit: 1000,
rateLimit: 100, // per minute
burstLimit: 200,
maxWebhooks: 3,
},
storage: {
maxLogoSize: 1024 * 1024, // 1MB
maxCustomCSSSize: 50 * 1024, // 50KB
auditLogRetention: 90, // days
maxCustomFields: 10,
},
features: {
maxRoles: 10,
maxPermissions: 50,
maxIntegrations: 5,
maxDomains: 1,
},
};
const defaultCompliance: OrganizationCompliance = {
dataRetention: {
userDataRetention: 365,
auditLogRetention: 90,
sessionLogRetention: 30,
automaticDeletion: false,
},
privacy: {
gdprCompliant: false,
ccpaCompliant: false,
hipaaCompliant: false,
soc2Compliant: false,
dataProcessingAgreement: false,
},
security: {
encryptionAtRest: true,
encryptionInTransit: true,
keyRotation: false,
backupEncryption: false,
accessLogging: true,
},
reporting: {
complianceReports: false,
auditReports: false,
securityReports: false,
dataExport: false,
rightToBeForgotten: false,
},
};
return {
...DEFAULT_ORGANIZATION_CONFIG,
...config,
features: { ...defaultFeatures, ...config.features },
limits: { ...defaultLimits, ...config.limits },
compliance: { ...defaultCompliance, ...config.compliance },
tier: config.tier || 'free',
isActive: config.isActive ?? true,
subscriptionStatus: config.subscriptionStatus || 'active',
usage: {
currentUsers: 0,
currentEndUsers: 0,
monthlyApiRequests: 0,
storageUsed: 0,
lastActivityAt: new Date(),
...config.usage,
},
} as ExtendedOrganizationConfig;
}
private applyOrganizationBranding(): void {
if (this.config.settings.branding) {
// Apply to theme manager
this.themeManager.applyBranding({
logo: {
url: this.config.settings.branding.logo,
alt: this.config.name,
},
colors: {
primary: this.config.settings.branding.primaryColor || '#3b82f6',
secondary: this.config.settings.branding.secondaryColor || '#64748b',
},
fonts: {
primary: 'Inter, ui-sans-serif, system-ui, sans-serif',
},
customCSS: this.config.settings.branding.customCSS,
});
// Apply to appearance manager
this.appearanceManager.applyOrganizationBranding(this.config);
}
}
private generateCustomTheme(): Partial<Theme> {
if (!this.isFeatureEnabled('ui.customThemes')) return {};
return this.themeManager.getTheme();
}
private generateCustomAppearance(): Partial<AppearanceConfig> {
if (!this.isFeatureEnabled('ui.customThemes')) return {};
return this.appearanceManager.getConfig();
}
private generateLocalizationConfig(): Partial<LocalizationConfig> {
if (!this.isFeatureEnabled('ui.localization')) return {};
// Return basic localization config
// In a real implementation, this might be customizable per organization
return {
defaultLocale: 'en',
supportedLocales: ['en', 'es', 'fr'],
};
}
private isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
private notifyListeners(): void {
this.listeners.forEach(callback => callback(this.config));
}
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Create organization configuration manager
*/
export function createOrganizationConfigManager(
config: Partial<ExtendedOrganizationConfig>,
themeManager?: ThemeManager,
appearanceManager?: AppearanceManager
): OrganizationConfigManager {
return new OrganizationConfigManager(config, themeManager, appearanceManager);
}
/**
* Transform organization settings from API to UI config
*/
export function transformOrganizationSettings(
apiSettings: any
): Partial<ExtendedOrganizationConfig> {
return {
id: apiSettings.id,
name: apiSettings.name,
slug: apiSettings.slug,
settings: {
allowPublicSignup: apiSettings.allowPublicSignup,
requireEmailVerification: apiSettings.requireEmailVerification,
requirePhoneVerification: apiSettings.requirePhoneVerification,
allowedDomains: apiSettings.allowedDomains,
mfaRequired: apiSettings.mfaSettings?.required,
allowedMfaMethods: apiSettings.mfaSettings?.allowedMethods || [],
passwordPolicy: apiSettings.passwordPolicy,
sessionSettings: apiSettings.sessionSettings,
branding: apiSettings.branding,
customFields: apiSettings.customFields,
},
features: {
// Map API features to UI feature flags
authentication: {
signUp: apiSettings.allowPublicSignup,
signIn: true,
passwordReset: true,
emailVerification: apiSettings.requireEmailVerification,
phoneVerification: apiSettings.requirePhoneVerification,
socialAuth: apiSettings.ssoEnabled,
magicLink: apiSettings.features?.magicLink || false,
passkeys: apiSettings.features?.passkeys || false,
},
security: {
mfa: apiSettings.mfaSettings?.enabled || false,
mfaRequired: apiSettings.mfaSettings?.required || false,
sso: apiSettings.ssoEnabled || false,
sessionManagement: true,
auditLogs: apiSettings.features?.auditLogs || false,
ipWhitelist: apiSettings.features?.ipWhitelist || false,
deviceTrust: apiSettings.features?.deviceTrust || false,
riskAssessment: apiSettings.features?.riskAssessment || false,
},
// ... other feature mappings
} as OrganizationFeatureFlags,
tier: apiSettings.plan?.tier || 'free',
isActive: apiSettings.active,
subscriptionStatus: apiSettings.subscription?.status || 'active',
usage: {
currentUsers: apiSettings.stats?.currentUsers || 0,
currentEndUsers: apiSettings.stats?.currentEndUsers || 0,
monthlyApiRequests: apiSettings.stats?.monthlyApiRequests || 0,
storageUsed: apiSettings.stats?.storageUsed || 0,
lastActivityAt: new Date(apiSettings.stats?.lastActivityAt || Date.now()),
},
};
}
/**
* Get feature availability by tier
*/
export function getFeaturesByTier(tier: 'free' | 'starter' | 'professional' | 'enterprise'): Partial<OrganizationFeatureFlags> {
const tierFeatures = {
free: {
authentication: { signUp: true, signIn: true, passwordReset: true },
security: { sessionManagement: true },
userManagement: { userProfiles: true, userInvitations: true },
organization: { memberManagement: true, invitations: true },
ui: { darkMode: true, localization: true },
},
starter: {
// All free features plus:
security: { mfa: true },
ui: { customThemes: true, logoUpload: true },
organization: { customBranding: true },
},
professional: {
// All starter features plus:
security: { sso: true, auditLogs: true },
organization: { webhooks: true, apiAccess: true, analytics: true },
integrations: { saml: true, oidc: true },
ui: { customCSS: true, colorCustomization: true },
},
enterprise: {
// All professional features plus:
security: { ipWhitelist: true, deviceTrust: true, riskAssessment: true },
organization: { customDomain: true },
integrations: { ldap: true, scim: true },
userManagement: { bulkUserOperations: true },
ui: { layoutCustomization: true },
},
};
// Merge features for the tier and all lower tiers
const tierOrder = ['free', 'starter', 'professional', 'enterprise'];
const tierIndex = tierOrder.indexOf(tier);
let features = {};
for (let i = 0; i <= tierIndex; i++) {
features = { ...features, ...tierFeatures[tierOrder[i] as keyof typeof tierFeatures] };
}
return features as Partial<OrganizationFeatureFlags>;
}
// ============================================================================
// Export organization utilities
// ============================================================================
export {
DEFAULT_ORGANIZATION_CONFIG,
};