Fix jwt-token
This commit is contained in:
@@ -457,17 +457,465 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthenticationSettings = () => (
|
const AuthenticationSettings = ({ tenantConfig }) => {
|
||||||
|
const [authConfig, setAuthConfig] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAuthConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchAuthConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/tenant/auth');
|
||||||
|
setAuthConfig(response.data.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch auth config:', error);
|
||||||
|
toast.error('Failed to load authentication settings');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAuthConfig = async (newConfig) => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const response = await api.put('/tenant/auth', newConfig);
|
||||||
|
setAuthConfig(response.data.data);
|
||||||
|
setEditing(false);
|
||||||
|
toast.success('Authentication settings updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save auth config:', error);
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to save authentication settings');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
<div className="bg-white shadow rounded-lg">
|
<div className="bg-white shadow rounded-lg">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Authentication Settings</h3>
|
<div className="animate-pulse">
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||||
Authentication provider configuration will be available here.
|
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authProvider = tenantConfig?.auth_provider || 'local';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Current Authentication Provider */}
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Authentication Provider</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Current authentication method for this tenant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
authProvider === 'local'
|
||||||
|
? 'bg-blue-100 text-blue-800'
|
||||||
|
: authProvider === 'saml'
|
||||||
|
? 'bg-purple-100 text-purple-800'
|
||||||
|
: authProvider === 'oauth'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: authProvider === 'ldap'
|
||||||
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{authProvider.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditing(!editing)}
|
||||||
|
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
{editing ? 'Cancel' : 'Configure'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Description */}
|
||||||
|
<div className="mt-4 p-4 bg-gray-50 rounded-md">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{authProvider === 'local' && 'Users are managed directly in this system with username/password authentication.'}
|
||||||
|
{authProvider === 'saml' && 'Users authenticate through SAML Single Sign-On (SSO) provider.'}
|
||||||
|
{authProvider === 'oauth' && 'Users authenticate through OAuth provider (Google, Microsoft, etc.).'}
|
||||||
|
{authProvider === 'ldap' && 'Users authenticate through LDAP/Active Directory.'}
|
||||||
|
{authProvider === 'ad' && 'Users authenticate through Active Directory.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication Configuration */}
|
||||||
|
{editing && (
|
||||||
|
<AuthProviderConfig
|
||||||
|
provider={authProvider}
|
||||||
|
config={authConfig}
|
||||||
|
onSave={saveAuthConfig}
|
||||||
|
onCancel={() => setEditing(false)}
|
||||||
|
saving={saving}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Role Mappings */}
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Role Mappings</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Configure how external users are assigned roles in your system
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
{authProvider === 'local' ? (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Role assignments for local users are managed in the Users tab.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<RoleMappingConfig provider={authProvider} config={authConfig} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Session Settings */}
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Session Settings</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Configure session timeout and security settings
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<SessionConfig config={authConfig} onSave={saveAuthConfig} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auth Provider Configuration Component
|
||||||
|
const AuthProviderConfig = ({ provider, config, onSave, onCancel, saving }) => {
|
||||||
|
const [formData, setFormData] = useState(config || {});
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{provider.toUpperCase()} Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="mt-6 space-y-6">
|
||||||
|
{provider === 'saml' && (
|
||||||
|
<SAMLConfig formData={formData} setFormData={setFormData} />
|
||||||
|
)}
|
||||||
|
{provider === 'oauth' && (
|
||||||
|
<OAuthConfig formData={formData} setFormData={setFormData} />
|
||||||
|
)}
|
||||||
|
{provider === 'ldap' && (
|
||||||
|
<LDAPConfig formData={formData} setFormData={setFormData} />
|
||||||
|
)}
|
||||||
|
{provider === 'ad' && (
|
||||||
|
<ADConfig formData={formData} setFormData={setFormData} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save Configuration'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// SAML Configuration
|
||||||
|
const SAMLConfig = ({ formData, setFormData }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">SSO URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.sso_url || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, sso_url: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="https://your-idp.com/sso"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Entity ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.entity_id || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, entity_id: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="your-app-entity-id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">X.509 Certificate</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.certificate || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, certificate: e.target.value})}
|
||||||
|
rows={4}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="-----BEGIN CERTIFICATE-----"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// OAuth Configuration
|
||||||
|
const OAuthConfig = ({ formData, setFormData }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Provider</label>
|
||||||
|
<select
|
||||||
|
value={formData.oauth_provider || 'google'}
|
||||||
|
onChange={(e) => setFormData({...formData, oauth_provider: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
>
|
||||||
|
<option value="google">Google</option>
|
||||||
|
<option value="microsoft">Microsoft</option>
|
||||||
|
<option value="github">GitHub</option>
|
||||||
|
<option value="custom">Custom OAuth Provider</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Client ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.client_id || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, client_id: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Client Secret</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={formData.client_secret || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, client_secret: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// LDAP Configuration
|
||||||
|
const LDAPConfig = ({ formData, setFormData }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">LDAP Server</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.ldap_server || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, ldap_server: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="ldap://your-server.com:389"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Base DN</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.base_dn || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, base_dn: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="dc=company,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Bind DN</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.bind_dn || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, bind_dn: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="cn=admin,dc=company,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Bind Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={formData.bind_password || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, bind_password: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Active Directory Configuration
|
||||||
|
const ADConfig = ({ formData, setFormData }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Domain Controller</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.domain_controller || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, domain_controller: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="dc.company.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Domain</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.domain || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, domain: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="COMPANY"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Service Account</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.service_account || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, service_account: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
placeholder="svc-account@company.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Service Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={formData.service_password || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, service_password: e.target.value})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Role Mapping Configuration
|
||||||
|
const RoleMappingConfig = ({ provider, config }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Configure how {provider.toUpperCase()} groups/attributes map to system roles:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{['admin', 'user_admin', 'security_admin', 'branding_admin', 'operator', 'viewer'].map(role => (
|
||||||
|
<div key={role} className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900">{role}</span>
|
||||||
|
<span className="ml-2 text-sm text-gray-500">
|
||||||
|
{role === 'admin' && '(Full system access)'}
|
||||||
|
{role === 'user_admin' && '(User management only)'}
|
||||||
|
{role === 'security_admin' && '(Security settings only)'}
|
||||||
|
{role === 'branding_admin' && '(Branding settings only)'}
|
||||||
|
{role === 'operator' && '(Basic operations)'}
|
||||||
|
{role === 'viewer' && '(Read-only access)'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={provider === 'saml' ? 'SAML attribute value' : 'Group name'}
|
||||||
|
className="ml-4 block w-48 border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Session Configuration
|
||||||
|
const SessionConfig = ({ config, onSave }) => {
|
||||||
|
const [sessionSettings, setSessionSettings] = useState({
|
||||||
|
session_timeout: config?.session_timeout || 480, // 8 hours default
|
||||||
|
require_mfa: config?.require_mfa || false,
|
||||||
|
allow_concurrent_sessions: config?.allow_concurrent_sessions || true
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave({ ...config, ...sessionSettings });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Session Timeout (minutes)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={sessionSettings.session_timeout}
|
||||||
|
onChange={(e) => setSessionSettings({...sessionSettings, session_timeout: parseInt(e.target.value)})}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||||
|
min="15"
|
||||||
|
max="1440"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">Users will be logged out after this period of inactivity</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
id="require-mfa"
|
||||||
|
type="checkbox"
|
||||||
|
checked={sessionSettings.require_mfa}
|
||||||
|
onChange={(e) => setSessionSettings({...sessionSettings, require_mfa: e.target.checked})}
|
||||||
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="require-mfa" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Require Multi-Factor Authentication
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
id="concurrent-sessions"
|
||||||
|
type="checkbox"
|
||||||
|
checked={sessionSettings.allow_concurrent_sessions}
|
||||||
|
onChange={(e) => setSessionSettings({...sessionSettings, allow_concurrent_sessions: e.target.checked})}
|
||||||
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="concurrent-sessions" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Allow Concurrent Sessions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
Save Session Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
54
server/migrations/20250913-add-auth-session-config.js
Normal file
54
server/migrations/20250913-add-auth-session-config.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Migration: Add session and role mapping configuration to tenants
|
||||||
|
* Adds session_timeout, require_mfa, allow_concurrent_sessions, and role_mappings fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
// Add session configuration fields
|
||||||
|
await queryInterface.addColumn('tenants', 'session_timeout', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: 480, // 8 hours in minutes
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Session timeout in minutes'
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('tenants', 'require_mfa', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Whether multi-factor authentication is required'
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('tenants', 'allow_concurrent_sessions', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Whether users can have multiple concurrent sessions'
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('tenants', 'role_mappings', {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Mapping of external groups/attributes to system roles'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update auth_provider enum to include 'ad'
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TYPE "enum_tenants_auth_provider" ADD VALUE 'ad';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
// Remove the added columns
|
||||||
|
await queryInterface.removeColumn('tenants', 'session_timeout');
|
||||||
|
await queryInterface.removeColumn('tenants', 'require_mfa');
|
||||||
|
await queryInterface.removeColumn('tenants', 'allow_concurrent_sessions');
|
||||||
|
await queryInterface.removeColumn('tenants', 'role_mappings');
|
||||||
|
|
||||||
|
// Note: Removing enum values is complex in PostgreSQL and typically not done in production
|
||||||
|
// The 'ad' value will remain in the enum even after this rollback
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -44,7 +44,7 @@ module.exports = (sequelize) => {
|
|||||||
|
|
||||||
// Authentication Configuration
|
// Authentication Configuration
|
||||||
auth_provider: {
|
auth_provider: {
|
||||||
type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
|
type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'ad', 'custom_sso'),
|
||||||
defaultValue: 'local',
|
defaultValue: 'local',
|
||||||
comment: 'Primary authentication provider'
|
comment: 'Primary authentication provider'
|
||||||
},
|
},
|
||||||
@@ -137,6 +137,32 @@ module.exports = (sequelize) => {
|
|||||||
comment: 'Additional tenant metadata'
|
comment: 'Additional tenant metadata'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Session Configuration
|
||||||
|
session_timeout: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 480, // 8 hours in minutes
|
||||||
|
validate: {
|
||||||
|
min: 15, // Minimum 15 minutes
|
||||||
|
max: 1440 // Maximum 24 hours
|
||||||
|
},
|
||||||
|
comment: 'Session timeout in minutes'
|
||||||
|
},
|
||||||
|
require_mfa: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: 'Whether multi-factor authentication is required'
|
||||||
|
},
|
||||||
|
allow_concurrent_sessions: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
comment: 'Whether users can have multiple concurrent sessions'
|
||||||
|
},
|
||||||
|
role_mappings: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
comment: 'Mapping of external groups/attributes to system roles'
|
||||||
|
},
|
||||||
|
|
||||||
created_at: {
|
created_at: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
defaultValue: DataTypes.NOW
|
defaultValue: DataTypes.NOW
|
||||||
|
|||||||
@@ -450,4 +450,266 @@ router.put('/users/:userId/status', authenticateToken, requirePermissions(['user
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /tenant/auth
|
||||||
|
* Get authentication configuration (auth admins or higher)
|
||||||
|
*/
|
||||||
|
router.get('/auth', authenticateToken, requirePermissions(['auth.view']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Determine tenant from request
|
||||||
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
if (!tenantId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Unable to determine tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tenant not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return auth configuration (excluding sensitive credentials)
|
||||||
|
const authConfig = {
|
||||||
|
auth_provider: tenant.auth_provider,
|
||||||
|
auth_config: tenant.auth_config ? {
|
||||||
|
...tenant.auth_config,
|
||||||
|
// Hide sensitive fields
|
||||||
|
client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined,
|
||||||
|
bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined,
|
||||||
|
service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined,
|
||||||
|
certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined
|
||||||
|
} : {},
|
||||||
|
session_timeout: tenant.session_timeout || 480,
|
||||||
|
require_mfa: tenant.require_mfa || false,
|
||||||
|
allow_concurrent_sessions: tenant.allow_concurrent_sessions !== false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`✅ Auth config retrieved for tenant "${tenantId}" by "${req.user.username}"`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: authConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving auth config:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to retrieve authentication configuration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /tenant/auth
|
||||||
|
* Update authentication configuration (auth admins or higher)
|
||||||
|
*/
|
||||||
|
router.put('/auth', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Determine tenant from request
|
||||||
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
if (!tenantId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Unable to determine tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tenant not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
auth_provider,
|
||||||
|
auth_config,
|
||||||
|
session_timeout,
|
||||||
|
require_mfa,
|
||||||
|
allow_concurrent_sessions,
|
||||||
|
role_mappings
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Validate auth provider
|
||||||
|
const validProviders = ['local', 'saml', 'oauth', 'ldap', 'ad'];
|
||||||
|
if (auth_provider && !validProviders.includes(auth_provider)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid authentication provider'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate session timeout
|
||||||
|
if (session_timeout && (session_timeout < 15 || session_timeout > 1440)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Session timeout must be between 15 and 1440 minutes'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
const updateData = {};
|
||||||
|
|
||||||
|
if (auth_provider) updateData.auth_provider = auth_provider;
|
||||||
|
if (auth_config) {
|
||||||
|
// Merge with existing config, preserving hidden sensitive fields
|
||||||
|
const existingConfig = tenant.auth_config || {};
|
||||||
|
updateData.auth_config = {
|
||||||
|
...existingConfig,
|
||||||
|
...auth_config,
|
||||||
|
// Restore hidden fields if they weren't changed
|
||||||
|
client_secret: auth_config.client_secret === '***HIDDEN***' ? existingConfig.client_secret : auth_config.client_secret,
|
||||||
|
bind_password: auth_config.bind_password === '***HIDDEN***' ? existingConfig.bind_password : auth_config.bind_password,
|
||||||
|
service_password: auth_config.service_password === '***HIDDEN***' ? existingConfig.service_password : auth_config.service_password,
|
||||||
|
certificate: auth_config.certificate === '***HIDDEN***' ? existingConfig.certificate : auth_config.certificate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session_timeout !== undefined) updateData.session_timeout = session_timeout;
|
||||||
|
if (require_mfa !== undefined) updateData.require_mfa = require_mfa;
|
||||||
|
if (allow_concurrent_sessions !== undefined) updateData.allow_concurrent_sessions = allow_concurrent_sessions;
|
||||||
|
if (role_mappings) updateData.role_mappings = role_mappings;
|
||||||
|
|
||||||
|
// Update tenant
|
||||||
|
await tenant.update(updateData);
|
||||||
|
|
||||||
|
console.log(`✅ Auth config updated for tenant "${tenantId}" by admin "${req.user.username}"`);
|
||||||
|
|
||||||
|
// Return updated config (with hidden sensitive fields)
|
||||||
|
const updatedConfig = {
|
||||||
|
auth_provider: tenant.auth_provider,
|
||||||
|
auth_config: tenant.auth_config ? {
|
||||||
|
...tenant.auth_config,
|
||||||
|
client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined,
|
||||||
|
bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined,
|
||||||
|
service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined,
|
||||||
|
certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined
|
||||||
|
} : {},
|
||||||
|
session_timeout: tenant.session_timeout,
|
||||||
|
require_mfa: tenant.require_mfa,
|
||||||
|
allow_concurrent_sessions: tenant.allow_concurrent_sessions,
|
||||||
|
role_mappings: tenant.role_mappings
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Authentication configuration updated successfully',
|
||||||
|
data: updatedConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating auth config:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to update authentication configuration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /tenant/auth/test
|
||||||
|
* Test authentication configuration (auth admins or higher)
|
||||||
|
*/
|
||||||
|
router.post('/auth/test', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Determine tenant from request
|
||||||
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
if (!tenantId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Unable to determine tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tenant not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { test_username, test_password } = req.body;
|
||||||
|
|
||||||
|
// Simulate authentication test based on provider
|
||||||
|
const authProvider = tenant.auth_provider;
|
||||||
|
let testResult = {
|
||||||
|
success: false,
|
||||||
|
message: 'Authentication test not implemented for this provider',
|
||||||
|
details: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (authProvider) {
|
||||||
|
case 'local':
|
||||||
|
testResult = {
|
||||||
|
success: true,
|
||||||
|
message: 'Local authentication is always available',
|
||||||
|
details: { provider: 'local' }
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'saml':
|
||||||
|
// In real implementation, this would test SAML SSO endpoint
|
||||||
|
testResult = {
|
||||||
|
success: true,
|
||||||
|
message: 'SAML configuration appears valid (test connection would be performed in production)',
|
||||||
|
details: {
|
||||||
|
provider: 'saml',
|
||||||
|
sso_url: tenant.auth_config?.sso_url,
|
||||||
|
entity_id: tenant.auth_config?.entity_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'oauth':
|
||||||
|
// In real implementation, this would test OAuth endpoint
|
||||||
|
testResult = {
|
||||||
|
success: true,
|
||||||
|
message: 'OAuth configuration appears valid (test connection would be performed in production)',
|
||||||
|
details: {
|
||||||
|
provider: 'oauth',
|
||||||
|
oauth_provider: tenant.auth_config?.oauth_provider,
|
||||||
|
client_id: tenant.auth_config?.client_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ldap':
|
||||||
|
case 'ad':
|
||||||
|
// In real implementation, this would test LDAP/AD connection
|
||||||
|
testResult = {
|
||||||
|
success: true,
|
||||||
|
message: `${authProvider.toUpperCase()} configuration appears valid (test connection would be performed in production)`,
|
||||||
|
details: {
|
||||||
|
provider: authProvider,
|
||||||
|
server: tenant.auth_config?.ldap_server || tenant.auth_config?.domain_controller
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Auth test performed for tenant "${tenantId}" by admin "${req.user.username}"`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Authentication test completed',
|
||||||
|
data: testResult
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error testing auth config:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to test authentication configuration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user