Fix jwt-token

This commit is contained in:
2025-09-12 23:06:34 +02:00
parent f34cc187f2
commit c7f4f23f00
24 changed files with 1933 additions and 1 deletions

View File

@@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react'
import api from '../services/api'
import { BuildingOfficeIcon, UsersIcon, ServerIcon, ChartBarIcon } from '@heroicons/react/24/outline'
const Dashboard = () => {
const [stats, setStats] = useState({
tenants: 0,
users: 0,
activeSessions: 0,
systemHealth: 'good'
})
const [loading, setLoading] = useState(true)
useEffect(() => {
loadDashboardData()
}, [])
const loadDashboardData = async () => {
try {
// Load basic stats
const [tenantsRes, usersRes] = await Promise.all([
api.get('/tenants?limit=1'),
api.get('/users?limit=1')
])
setStats({
tenants: tenantsRes.data.pagination?.total || 0,
users: usersRes.data.pagination?.total || 0,
activeSessions: Math.floor(Math.random() * 50) + 10, // Mock data
systemHealth: 'good'
})
} catch (error) {
console.error('Error loading dashboard data:', error)
} finally {
setLoading(false)
}
}
const statCards = [
{
name: 'Total Tenants',
value: stats.tenants,
icon: BuildingOfficeIcon,
color: 'bg-blue-500'
},
{
name: 'Total Users',
value: stats.users,
icon: UsersIcon,
color: 'bg-green-500'
},
{
name: 'Active Sessions',
value: stats.activeSessions,
icon: ChartBarIcon,
color: 'bg-yellow-500'
},
{
name: 'System Health',
value: stats.systemHealth === 'good' ? 'Good' : 'Issues',
icon: ServerIcon,
color: stats.systemHealth === 'good' ? 'bg-green-500' : 'bg-red-500'
}
]
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Overview of your UAMILS system</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{statCards.map((stat) => (
<div key={stat.name} className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className={`p-3 rounded-lg ${stat.color}`}>
<stat.icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">{stat.name}</p>
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
</div>
</div>
</div>
))}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div className="space-y-3">
<button className="w-full text-left px-4 py-3 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">
<div className="font-medium text-blue-900">Create New Tenant</div>
<div className="text-sm text-blue-700">Add a new organization to the system</div>
</button>
<button className="w-full text-left px-4 py-3 bg-green-50 hover:bg-green-100 rounded-lg transition-colors">
<div className="font-medium text-green-900">Manage Users</div>
<div className="text-sm text-green-700">View and edit user accounts</div>
</button>
<button className="w-full text-left px-4 py-3 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors">
<div className="font-medium text-purple-900">System Settings</div>
<div className="text-sm text-purple-700">Configure system-wide settings</div>
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3 text-sm">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-gray-600">New tenant "Acme Corp" created</span>
<span className="text-gray-400">2 hours ago</span>
</div>
<div className="flex items-center space-x-3 text-sm">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-gray-600">User "john.doe" logged in</span>
<span className="text-gray-400">4 hours ago</span>
</div>
<div className="flex items-center space-x-3 text-sm">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span className="text-gray-600">System backup completed</span>
<span className="text-gray-400">6 hours ago</span>
</div>
<div className="flex items-center space-x-3 text-sm">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span className="text-gray-600">SAML configuration updated</span>
<span className="text-gray-400">1 day ago</span>
</div>
</div>
</div>
</div>
</div>
)
}
export default Dashboard

View File

@@ -0,0 +1,125 @@
import React, { useState } from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
const Login = () => {
const { isAuthenticated, login } = useAuth()
const [formData, setFormData] = useState({
username: '',
password: ''
})
const [showPassword, setShowPassword] = useState(false)
const [loading, setLoading] = useState(false)
if (isAuthenticated) {
return <Navigate to="/dashboard" replace />
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
const result = await login(formData.username, formData.password)
if (!result.success) {
setLoading(false)
}
// If successful, the redirect will happen automatically
}
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
UAMILS Management Portal
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Sign in to manage tenants and system configuration
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="username" className="sr-only">
Username
</label>
<input
id="username"
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Username"
value={formData.username}
onChange={handleInputChange}
disabled={loading}
/>
</div>
<div className="relative">
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
required
className="appearance-none rounded-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={formData.password}
onChange={handleInputChange}
disabled={loading}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Signing in...
</div>
) : (
'Sign in'
)}
</button>
</div>
<div className="text-center">
<p className="text-xs text-gray-500">
Admin access required. Default: admin / admin123
</p>
</div>
</form>
</div>
</div>
)
}
export default Login

View File

@@ -0,0 +1,196 @@
import React, { useState, useEffect } from 'react'
import api from '../services/api'
import toast from 'react-hot-toast'
import {
CogIcon,
ServerIcon,
DatabaseIcon,
ShieldCheckIcon,
ClockIcon
} from '@heroicons/react/24/outline'
const System = () => {
const [systemInfo, setSystemInfo] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadSystemInfo()
}, [])
const loadSystemInfo = async () => {
try {
setLoading(true)
// This would be a real API endpoint in production
const response = await api.get('/system/info')
setSystemInfo(response.data.data)
} catch (error) {
// Mock data for development
setSystemInfo({
version: '1.0.0',
environment: 'development',
uptime: '7d 14h 32m',
database: {
status: 'connected',
version: 'PostgreSQL 14.2',
connections: 5,
maxConnections: 100
},
memory: {
used: '256MB',
total: '1GB',
percentage: 25
},
lastBackup: '2024-01-15T10:30:00Z',
ssl: {
status: 'valid',
expiresAt: '2024-03-15T00:00:00Z'
}
})
} finally {
setLoading(false)
}
}
const StatusCard = ({ title, icon: Icon, children }) => (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center mb-4">
<Icon className="h-6 w-6 text-blue-600 mr-2" />
<h3 className="text-lg font-medium text-gray-900">{title}</h3>
</div>
{children}
</div>
)
const StatusIndicator = ({ status }) => {
const colors = {
connected: 'bg-green-100 text-green-800',
valid: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800'
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[status] || colors.error}`}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
)
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">System</h1>
<p className="text-gray-600">Monitor system health and configuration</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<StatusCard title="Server Status" icon={ServerIcon}>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Version</span>
<span className="text-sm font-medium">{systemInfo.version}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Environment</span>
<span className="text-sm font-medium capitalize">{systemInfo.environment}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Uptime</span>
<span className="text-sm font-medium">{systemInfo.uptime}</span>
</div>
</div>
</StatusCard>
<StatusCard title="Database" icon={DatabaseIcon}>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Status</span>
<StatusIndicator status={systemInfo.database.status} />
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Version</span>
<span className="text-sm font-medium">{systemInfo.database.version}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Connections</span>
<span className="text-sm font-medium">
{systemInfo.database.connections}/{systemInfo.database.maxConnections}
</span>
</div>
</div>
</StatusCard>
<StatusCard title="SSL Certificate" icon={ShieldCheckIcon}>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500">Status</span>
<StatusIndicator status={systemInfo.ssl.status} />
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500">Expires</span>
<span className="text-sm font-medium">
{new Date(systemInfo.ssl.expiresAt).toLocaleDateString()}
</span>
</div>
</div>
</StatusCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<CogIcon className="h-5 w-5 text-blue-600 mr-2" />
Memory Usage
</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Used</span>
<span className="font-medium">{systemInfo.memory.used}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Total</span>
<span className="font-medium">{systemInfo.memory.total}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${systemInfo.memory.percentage}%` }}
></div>
</div>
<div className="text-xs text-gray-500 text-center">
{systemInfo.memory.percentage}% used
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<ClockIcon className="h-5 w-5 text-blue-600 mr-2" />
Last Backup
</h3>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">
{new Date(systemInfo.lastBackup).toLocaleDateString()}
</div>
<div className="text-sm text-gray-500">
{new Date(systemInfo.lastBackup).toLocaleTimeString()}
</div>
<button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
Run Backup Now
</button>
</div>
</div>
</div>
</div>
)
}
export default System

View File

@@ -0,0 +1,312 @@
import React, { useState, useEffect } from 'react'
import api from '../services/api'
import toast from 'react-hot-toast'
import {
PlusIcon,
PencilIcon,
TrashIcon,
MagnifyingGlassIcon,
BuildingOfficeIcon
} from '@heroicons/react/24/outline'
const Tenants = () => {
const [tenants, setTenants] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [showCreateModal, setShowCreateModal] = useState(false)
const [editingTenant, setEditingTenant] = useState(null)
const [pagination, setPagination] = useState({
total: 0,
limit: 10,
offset: 0,
pages: 0
})
useEffect(() => {
loadTenants()
}, [])
const loadTenants = async (offset = 0, search = '') => {
try {
setLoading(true)
const params = new URLSearchParams({
limit: pagination.limit.toString(),
offset: offset.toString()
})
if (search) {
params.append('search', search)
}
const response = await api.get(`/tenants?${params}`)
setTenants(response.data.data)
setPagination(response.data.pagination)
} catch (error) {
toast.error('Failed to load tenants')
console.error('Error loading tenants:', error)
} finally {
setLoading(false)
}
}
const handleSearch = (e) => {
const term = e.target.value
setSearchTerm(term)
loadTenants(0, term)
}
const deleteTenant = async (tenantId) => {
if (!confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) {
return
}
try {
await api.delete(`/tenants/${tenantId}`)
toast.success('Tenant deleted successfully')
loadTenants()
} catch (error) {
toast.error('Failed to delete tenant')
console.error('Error deleting tenant:', error)
}
}
const getAuthProviderBadge = (provider) => {
const colors = {
local: 'bg-gray-100 text-gray-800',
saml: 'bg-blue-100 text-blue-800',
oauth: 'bg-green-100 text-green-800',
ldap: 'bg-yellow-100 text-yellow-800',
custom_sso: 'bg-purple-100 text-purple-800'
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[provider] || colors.local}`}>
{provider.toUpperCase()}
</span>
)
}
const getSubscriptionBadge = (type) => {
const colors = {
free: 'bg-gray-100 text-gray-800',
basic: 'bg-blue-100 text-blue-800',
premium: 'bg-purple-100 text-purple-800',
enterprise: 'bg-green-100 text-green-800'
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[type] || colors.basic}`}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</span>
)
}
if (loading && tenants.length === 0) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div>
{/* Header */}
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Tenants</h1>
<p className="text-gray-600">Manage organizations and their configurations</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2"
>
<PlusIcon className="h-5 w-5" />
<span>Create Tenant</span>
</button>
</div>
</div>
{/* Search */}
<div className="mb-6">
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search tenants..."
value={searchTerm}
onChange={handleSearch}
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-full max-w-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* Tenants Table */}
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tenant
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Domain
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Auth Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Subscription
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Users
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tenants.map((tenant) => (
<tr key={tenant.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<BuildingOfficeIcon className="h-6 w-6 text-blue-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{tenant.name}</div>
<div className="text-sm text-gray-500">{tenant.slug}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{tenant.domain || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getAuthProviderBadge(tenant.auth_provider)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getSubscriptionBadge(tenant.subscription_type)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{tenant.users?.length || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(tenant.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => setEditingTenant(tenant)}
className="text-blue-600 hover:text-blue-900 p-1 rounded"
title="Edit"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => deleteTenant(tenant.id)}
className="text-red-600 hover:text-red-900 p-1 rounded"
title="Delete"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => loadTenants(Math.max(0, pagination.offset - pagination.limit), searchTerm)}
disabled={pagination.offset === 0}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => loadTenants(pagination.offset + pagination.limit, searchTerm)}
disabled={pagination.offset + pagination.limit >= pagination.total}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{pagination.offset + 1}</span> to{' '}
<span className="font-medium">
{Math.min(pagination.offset + pagination.limit, pagination.total)}
</span>{' '}
of <span className="font-medium">{pagination.total}</span> results
</p>
</div>
</div>
</div>
)}
{tenants.length === 0 && !loading && (
<div className="text-center py-12">
<BuildingOfficeIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No tenants</h3>
<p className="mt-1 text-sm text-gray-500">Get started by creating a new tenant.</p>
<div className="mt-6">
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<PlusIcon className="h-5 w-5 mr-2" />
Create Tenant
</button>
</div>
</div>
)}
</div>
{/* Modals would go here */}
{showCreateModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Create New Tenant</h3>
<p className="text-sm text-gray-500 mb-4">
Tenant creation modal would go here with form fields for name, slug, domain, auth provider, etc.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={() => {
setShowCreateModal(false)
toast.success('Tenant creation modal - implement form handling')
}}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Create
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}
export default Tenants

View File

@@ -0,0 +1,149 @@
import React, { useState, useEffect } from 'react'
import api from '../services/api'
import toast from 'react-hot-toast'
import { UsersIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
const Users = () => {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
useEffect(() => {
loadUsers()
}, [])
const loadUsers = async () => {
try {
setLoading(true)
const response = await api.get('/users')
setUsers(response.data.data || [])
} catch (error) {
toast.error('Failed to load users')
console.error('Error loading users:', error)
} finally {
setLoading(false)
}
}
const getRoleBadge = (role) => {
const colors = {
admin: 'bg-red-100 text-red-800',
operator: 'bg-blue-100 text-blue-800',
viewer: 'bg-gray-100 text-gray-800'
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[role] || colors.viewer}`}>
{role.charAt(0).toUpperCase() + role.slice(1)}
</span>
)
}
const filteredUsers = users.filter(user =>
user.username?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase())
)
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div>
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Users</h1>
<p className="text-gray-600">Manage user accounts across all tenants</p>
</div>
<div className="mb-6">
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-full max-w-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Login
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
<span className="text-sm font-medium text-blue-600">
{user.username?.charAt(0).toUpperCase()}
</span>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{user.username}</div>
<div className="text-sm text-gray-500">{user.email}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getRoleBadge(user.role)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(user.created_at).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
{filteredUsers.length === 0 && (
<div className="text-center py-12">
<UsersIcon 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">
{searchTerm ? 'Try adjusting your search criteria.' : 'No users have been created yet.'}
</p>
</div>
)}
</div>
</div>
)
}
export default Users