Fix jwt-token

This commit is contained in:
2025-09-13 15:42:55 +02:00
parent cbb2586dca
commit 8a6a0a472c
3 changed files with 328 additions and 69 deletions

View File

@@ -12,6 +12,7 @@ import {
EyeIcon,
EyeSlashIcon
} from '@heroicons/react/24/outline';
import { hasPermission, canAccessSettings } from '../utils/rbac';
const Settings = () => {
const { user } = useAuth();
@@ -20,8 +21,8 @@ const Settings = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Check if user has admin role
const isAdmin = user?.role === 'admin';
// Check if user can access settings based on RBAC permissions
const canAccess = canAccessSettings(user?.role);
useEffect(() => {
fetchTenantConfig();
@@ -48,27 +49,60 @@ const Settings = () => {
);
}
if (!isAdmin) {
if (!canAccess) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<ShieldCheckIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Access Denied</h3>
<p className="mt-1 text-sm text-gray-500">
You need admin privileges to access tenant settings.
You don't have permission to access tenant settings.
</p>
</div>
</div>
);
}
const tabs = [
{ id: 'general', name: 'General', icon: CogIcon },
{ id: 'branding', name: 'Branding', icon: PaintBrushIcon },
{ id: 'security', name: 'Security', icon: ShieldCheckIcon },
{ id: 'authentication', name: 'Authentication', icon: KeyIcon },
{ id: 'users', name: 'Users', icon: UserGroupIcon },
];
// Filter tabs based on user permissions
const availableTabs = [
{
id: 'general',
name: 'General',
icon: CogIcon,
permission: 'tenant.view'
},
{
id: 'branding',
name: 'Branding',
icon: PaintBrushIcon,
permission: 'branding.view'
},
{
id: 'security',
name: 'Security',
icon: ShieldCheckIcon,
permission: 'security.view'
},
{
id: 'authentication',
name: 'Authentication',
icon: KeyIcon,
permission: 'auth.view'
},
{
id: 'users',
name: 'Users',
icon: UserGroupIcon,
permission: 'users.view'
},
].filter(tab => hasPermission(user?.role, tab.permission));
// Set initial tab to first available tab
useEffect(() => {
if (availableTabs.length > 0 && !availableTabs.find(tab => tab.id === activeTab)) {
setActiveTab(availableTabs[0].id);
}
}, [availableTabs, activeTab]);
return (
<div className="min-h-screen bg-gray-50">
@@ -81,7 +115,7 @@ const Settings = () => {
</h3>
<div className="mt-4 sm:mt-0 sm:ml-10">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => {
{availableTabs.map((tab) => {
const Icon = tab.icon;
return (
<button
@@ -104,11 +138,21 @@ const Settings = () => {
</div>
<div className="mt-6">
{activeTab === 'general' && <GeneralSettings tenantConfig={tenantConfig} />}
{activeTab === 'branding' && <BrandingSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
{activeTab === 'security' && <SecuritySettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
{activeTab === 'authentication' && <AuthenticationSettings tenantConfig={tenantConfig} />}
{activeTab === 'users' && <UsersSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
{activeTab === 'general' && hasPermission(user?.role, 'tenant.view') && (
<GeneralSettings tenantConfig={tenantConfig} />
)}
{activeTab === 'branding' && hasPermission(user?.role, 'branding.view') && (
<BrandingSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
)}
{activeTab === 'security' && hasPermission(user?.role, 'security.view') && (
<SecuritySettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
)}
{activeTab === 'authentication' && hasPermission(user?.role, 'auth.view') && (
<AuthenticationSettings tenantConfig={tenantConfig} />
)}
{activeTab === 'users' && hasPermission(user?.role, 'users.view') && (
<UsersSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
)}
</div>
</div>
</div>
@@ -141,6 +185,7 @@ const GeneralSettings = ({ tenantConfig }) => (
// Branding Settings Component
const BrandingSettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const [branding, setBranding] = useState({
logo_url: '',
primary_color: '#3B82F6',
@@ -151,6 +196,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
const [uploading, setUploading] = useState(false);
const [logoPreview, setLogoPreview] = useState(null);
const canEdit = hasPermission(user?.role, 'branding.edit');
useEffect(() => {
if (tenantConfig?.branding) {
setBranding(tenantConfig.branding);
@@ -231,7 +278,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="text"
value={branding.company_name}
onChange={(e) => setBranding(prev => ({ ...prev, company_name: e.target.value }))}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
disabled={!canEdit}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
</div>
@@ -259,12 +307,12 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
<input
type="file"
accept="image/*"
disabled={!canEdit || uploading}
onChange={(e) => {
handleFilePreview(e);
handleLogoUpload(e);
}}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
disabled={uploading}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 disabled:opacity-50"
/>
<p className="text-xs text-gray-500 mt-1">PNG, JPG up to 5MB</p>
</div>
@@ -296,7 +344,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="url"
value={branding.logo_url}
onChange={(e) => setBranding(prev => ({ ...prev, logo_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"
disabled={!canEdit}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
placeholder="https://example.com/logo.png"
/>
</div>
@@ -310,13 +359,15 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="color"
value={branding.primary_color}
onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
className="h-10 w-20 border border-gray-300 rounded-md"
disabled={!canEdit}
className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50"
/>
<input
type="text"
value={branding.primary_color}
onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
disabled={!canEdit}
className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
</div>
</div>
@@ -328,26 +379,34 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="color"
value={branding.secondary_color}
onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
className="h-10 w-20 border border-gray-300 rounded-md"
disabled={!canEdit}
className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50"
/>
<input
type="text"
value={branding.secondary_color}
onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
disabled={!canEdit}
className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
</div>
</div>
</div>
<div className="flex justify-end">
<button
onClick={handleSave}
disabled={saving}
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Branding'}
</button>
{canEdit ? (
<button
onClick={handleSave}
disabled={saving}
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Branding'}
</button>
) : (
<div className="text-sm text-gray-500 py-2">
You don't have permission to edit branding settings
</div>
)}
</div>
</div>
</div>
@@ -357,6 +416,7 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
// Placeholder components for other tabs
const SecuritySettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const [securitySettings, setSecuritySettings] = useState({
ip_restriction_enabled: false,
ip_whitelist: [],
@@ -365,6 +425,8 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
const [newIP, setNewIP] = useState('');
const [saving, setSaving] = useState(false);
const canEdit = hasPermission(user?.role, 'security.edit');
useEffect(() => {
if (tenantConfig) {
setSecuritySettings({
@@ -432,7 +494,7 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
<h3 className="text-lg leading-6 font-medium text-gray-900">Security Settings</h3>
<button
onClick={handleSave}
disabled={saving}
disabled={saving || !canEdit}
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Changes'}
@@ -453,11 +515,12 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
type="checkbox"
id="ip_restriction_enabled"
checked={securitySettings.ip_restriction_enabled}
disabled={!canEdit}
onChange={(e) => setSecuritySettings(prev => ({
...prev,
ip_restriction_enabled: e.target.checked
}))}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded disabled:opacity-50"
/>
<label htmlFor="ip_restriction_enabled" className="ml-2 text-sm font-medium text-gray-700">
Enable IP Restrictions
@@ -566,6 +629,9 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
};
const AuthenticationSettings = ({ tenantConfig }) => {
const { user } = useAuth();
const canEdit = hasPermission(user?.role, 'auth.edit');
const canView = hasPermission(user?.role, 'auth.view');
const [authConfig, setAuthConfig] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -1025,6 +1091,7 @@ const SessionConfig = ({ config, onSave }) => {
};
const UsersSettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateUser, setShowCreateUser] = useState(false);
@@ -1032,7 +1099,14 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
const [editingUser, setEditingUser] = useState(null);
const authProvider = tenantConfig?.auth_provider;
const canManageUsers = authProvider === 'local'; // Only local auth allows user management
const isLocalAuth = authProvider === 'local';
// Check RBAC permissions for user management
const canViewUsers = hasPermission(user?.role, 'users.view');
const canCreateUsers = hasPermission(user?.role, 'users.create') && isLocalAuth;
const canEditUsers = hasPermission(user?.role, 'users.edit') && isLocalAuth;
const canDeleteUsers = hasPermission(user?.role, 'users.delete') && isLocalAuth;
const canManageUsers = canCreateUsers || canEditUsers; // Show management UI if can create or edit
useEffect(() => {
fetchUsers();
@@ -1073,13 +1147,15 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">User Management</h3>
<p className="text-sm text-gray-500">
{authProvider === 'local'
? 'Manage local users for this tenant'
{isLocalAuth
? canManageUsers
? 'Manage local users for this tenant'
: 'View users for this tenant'
: `Users are managed through ${authProvider.toUpperCase()}. Showing read-only information.`
}
</p>
</div>
{canManageUsers && (
{canCreateUsers && (
<button
onClick={() => setShowCreateUser(true)}
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700"
@@ -1113,8 +1189,10 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
<UserGroupIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No users found</h3>
<p className="mt-1 text-sm text-gray-500">
{canManageUsers
? 'Get started by creating a new user.'
{isLocalAuth
? canCreateUsers
? 'Get started by creating a new user.'
: 'No users have been created yet.'
: 'Users will appear here when they log in through your authentication provider.'
}
</p>
@@ -1136,7 +1214,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Login
</th>
{canManageUsers && (
{(canEditUsers || canDeleteUsers) && (
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
@@ -1178,23 +1256,27 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
: 'Never'
}
</td>
{canManageUsers && (
{(canEditUsers || canDeleteUsers) && (
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => handleEditUser(user)}
className="text-primary-600 hover:text-primary-900 mr-4"
>
Edit
</button>
<button
onClick={() => handleToggleUserStatus(user)}
className={user.is_active
? 'text-red-600 hover:text-red-900'
: 'text-green-600 hover:text-green-900'
}
>
{user.is_active ? 'Deactivate' : 'Activate'}
</button>
{canEditUsers && (
<button
onClick={() => handleEditUser(user)}
className="text-primary-600 hover:text-primary-900 mr-4"
>
Edit
</button>
)}
{canDeleteUsers && (
<button
onClick={() => handleToggleUserStatus(user)}
className={user.is_active
? 'text-red-600 hover:text-red-900'
: 'text-green-600 hover:text-green-900'
}
>
{user.is_active ? 'Deactivate' : 'Activate'}
</button>
)}
</td>
)}
</tr>
@@ -1205,7 +1287,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
)}
{/* Non-Local Auth Guidance */}
{!canManageUsers && (
{!isLocalAuth && (
<div className="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-2">
User Management for {authProvider?.toUpperCase()}
@@ -1238,7 +1320,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
</div>
{/* Create User Modal for Local Auth */}
{showCreateUser && canManageUsers && (
{showCreateUser && canCreateUsers && (
<CreateUserModal
isOpen={showCreateUser}
onClose={() => setShowCreateUser(false)}
@@ -1250,7 +1332,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
)}
{/* Edit User Modal for Local Auth */}
{showEditUser && canManageUsers && editingUser && (
{showEditUser && canEditUsers && editingUser && (
<EditUserModal
isOpen={showEditUser}
onClose={() => {