256 lines
7.4 KiB
JavaScript
256 lines
7.4 KiB
JavaScript
/**
|
|
* Multi-Tenant Authentication Context
|
|
* Handles authentication for different tenants and providers
|
|
*/
|
|
|
|
import React, { createContext, useContext, useReducer, useEffect } from 'react';
|
|
import api from '../services/api';
|
|
|
|
const MultiTenantAuthContext = createContext();
|
|
|
|
const authReducer = (state, action) => {
|
|
switch (action.type) {
|
|
case 'SET_LOADING':
|
|
return { ...state, loading: action.payload };
|
|
case 'SET_TENANT':
|
|
return { ...state, tenant: action.payload };
|
|
case 'SET_AUTH_CONFIG':
|
|
return { ...state, authConfig: action.payload };
|
|
case 'LOGIN_START':
|
|
return { ...state, loading: true, error: null };
|
|
case 'LOGIN_SUCCESS':
|
|
return {
|
|
...state,
|
|
user: action.payload.user,
|
|
token: action.payload.token,
|
|
tenant: action.payload.tenant,
|
|
isAuthenticated: true,
|
|
loading: false,
|
|
error: null
|
|
};
|
|
case 'LOGIN_FAILURE':
|
|
return { ...state, loading: false, error: action.payload, isAuthenticated: false };
|
|
case 'LOGOUT':
|
|
return { ...state, user: null, token: null, isAuthenticated: false, loading: false };
|
|
case 'SET_ERROR':
|
|
return { ...state, error: action.payload, loading: false };
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
const initialState = {
|
|
user: null,
|
|
token: localStorage.getItem('token'),
|
|
tenant: null,
|
|
authConfig: null,
|
|
isAuthenticated: false,
|
|
loading: true,
|
|
error: null
|
|
};
|
|
|
|
export const MultiTenantAuthProvider = ({ children }) => {
|
|
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
|
|
// Determine tenant from URL or storage
|
|
const determineTenant = () => {
|
|
// Method 1: Subdomain
|
|
const hostname = window.location.hostname;
|
|
const subdomain = hostname.split('.')[0];
|
|
if (subdomain && subdomain !== 'www' && subdomain !== 'localhost' && !hostname.includes('127.0.0.1')) {
|
|
return subdomain;
|
|
}
|
|
|
|
// Method 2: URL parameter
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get('tenant')) {
|
|
return urlParams.get('tenant');
|
|
}
|
|
|
|
// Method 3: Local storage
|
|
const storedTenant = localStorage.getItem('tenant');
|
|
if (storedTenant) {
|
|
return storedTenant;
|
|
}
|
|
|
|
// Default tenant
|
|
return 'default';
|
|
};
|
|
|
|
// Initialize authentication
|
|
useEffect(() => {
|
|
const initAuth = async () => {
|
|
try {
|
|
const tenantId = determineTenant();
|
|
dispatch({ type: 'SET_TENANT', payload: tenantId });
|
|
localStorage.setItem('tenant', tenantId);
|
|
|
|
// Get tenant auth configuration
|
|
const authConfigResponse = await api.get(`/auth/config/${tenantId}`);
|
|
dispatch({ type: 'SET_AUTH_CONFIG', payload: authConfigResponse.data.data });
|
|
|
|
// Check if user is already authenticated
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
// Validate token with current tenant context
|
|
const profileResponse = await api.get('/users/profile');
|
|
dispatch({
|
|
type: 'LOGIN_SUCCESS',
|
|
payload: {
|
|
user: profileResponse.data.data,
|
|
token: token,
|
|
tenant: tenantId
|
|
}
|
|
});
|
|
} else {
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth initialization error:', error);
|
|
localStorage.removeItem('token');
|
|
dispatch({ type: 'SET_LOADING', payload: false });
|
|
}
|
|
};
|
|
|
|
initAuth();
|
|
}, []);
|
|
|
|
// Handle URL token (from SSO redirects)
|
|
useEffect(() => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const urlToken = urlParams.get('token');
|
|
const urlTenant = urlParams.get('tenant');
|
|
|
|
if (urlToken && urlTenant) {
|
|
localStorage.setItem('token', urlToken);
|
|
localStorage.setItem('tenant', urlTenant);
|
|
|
|
// Clean URL
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Reload to reinitialize with new token
|
|
window.location.reload();
|
|
}
|
|
}, []);
|
|
|
|
// Login function
|
|
const login = async (credentials) => {
|
|
try {
|
|
dispatch({ type: 'LOGIN_START' });
|
|
|
|
const tenantId = state.tenant || determineTenant();
|
|
|
|
// For local authentication, use existing login endpoint
|
|
if (state.authConfig?.provider === 'local' || state.authConfig?.provider === 'ldap') {
|
|
const response = await api.post('/auth/login', {
|
|
...credentials,
|
|
tenant: tenantId
|
|
});
|
|
|
|
const { user, token } = response.data.data;
|
|
localStorage.setItem('token', token);
|
|
localStorage.setItem('tenant', tenantId);
|
|
|
|
dispatch({
|
|
type: 'LOGIN_SUCCESS',
|
|
payload: { user, token, tenant: tenantId }
|
|
});
|
|
|
|
return { success: true };
|
|
} else {
|
|
// For SSO providers, redirect to SSO endpoint
|
|
const ssoUrl = getSSOLoginUrl(state.authConfig, tenantId);
|
|
window.location.href = ssoUrl;
|
|
return { success: true, redirect: true };
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error.response?.data?.message || 'Login failed';
|
|
dispatch({ type: 'LOGIN_FAILURE', payload: errorMessage });
|
|
return { success: false, error: errorMessage };
|
|
}
|
|
};
|
|
|
|
// SSO Login
|
|
const loginWithSSO = () => {
|
|
const tenantId = state.tenant || determineTenant();
|
|
const ssoUrl = getSSOLoginUrl(state.authConfig, tenantId);
|
|
window.location.href = ssoUrl;
|
|
};
|
|
|
|
// Get SSO login URL
|
|
const getSSOLoginUrl = (authConfig, tenantId) => {
|
|
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
|
|
|
switch (authConfig?.provider) {
|
|
case 'saml':
|
|
return `/auth/saml/${tenantId}/login?returnUrl=${returnUrl}`;
|
|
case 'oauth':
|
|
return `/auth/oauth/${tenantId}/login?returnUrl=${returnUrl}`;
|
|
default:
|
|
return `/login?tenant=${tenantId}`;
|
|
}
|
|
};
|
|
|
|
// Logout
|
|
const logout = async () => {
|
|
try {
|
|
const tenantId = state.tenant;
|
|
|
|
// Call logout endpoint
|
|
const response = await api.post('/auth/logout');
|
|
|
|
// Clear local storage
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('tenant');
|
|
|
|
dispatch({ type: 'LOGOUT' });
|
|
|
|
// Handle provider-specific logout
|
|
if (response.data.logout_url) {
|
|
window.location.href = response.data.logout_url;
|
|
} else {
|
|
window.location.href = `/login?tenant=${tenantId}`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
// Force logout even if API call fails
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('tenant');
|
|
dispatch({ type: 'LOGOUT' });
|
|
window.location.href = '/login';
|
|
}
|
|
};
|
|
|
|
// Switch tenant
|
|
const switchTenant = (newTenantId) => {
|
|
localStorage.setItem('tenant', newTenantId);
|
|
localStorage.removeItem('token'); // Clear token when switching tenants
|
|
window.location.href = `/login?tenant=${newTenantId}`;
|
|
};
|
|
|
|
const value = {
|
|
...state,
|
|
login,
|
|
logout,
|
|
loginWithSSO,
|
|
switchTenant,
|
|
getSSOLoginUrl
|
|
};
|
|
|
|
return (
|
|
<MultiTenantAuthContext.Provider value={value}>
|
|
{children}
|
|
</MultiTenantAuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useMultiTenantAuth = () => {
|
|
const context = useContext(MultiTenantAuthContext);
|
|
if (!context) {
|
|
throw new Error('useMultiTenantAuth must be used within a MultiTenantAuthProvider');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
export default MultiTenantAuthContext;
|