Fix jwt-token

This commit is contained in:
2025-09-13 13:05:28 +02:00
parent 96ab92a20d
commit 04b103fe32
5 changed files with 1118 additions and 1 deletions

View File

@@ -0,0 +1,389 @@
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
PlusIcon,
PencilIcon,
TrashIcon,
ArrowLeftIcon,
MagnifyingGlassIcon,
UserIcon
} from '@heroicons/react/24/outline'
import api from '../services/api'
import toast from 'react-hot-toast'
import UserModal from '../components/UserModal'
const TenantUsersPage = () => {
const { tenantId } = useParams()
const navigate = useNavigate()
const [tenant, setTenant] = useState(null)
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [selectedRole, setSelectedRole] = useState('')
const [showUserModal, setShowUserModal] = useState(false)
const [editingUser, setEditingUser] = useState(null)
const [pagination, setPagination] = useState({
total: 0,
limit: 10,
offset: 0,
pages: 0
})
useEffect(() => {
loadUsers()
}, [tenantId, searchTerm, selectedRole])
const loadUsers = async (offset = 0) => {
try {
setLoading(true)
const params = new URLSearchParams({
limit: pagination.limit.toString(),
offset: offset.toString()
})
if (searchTerm) params.append('search', searchTerm)
if (selectedRole) params.append('role', selectedRole)
const response = await api.get(`/management/tenants/${tenantId}/users?${params}`)
setUsers(response.data.data)
setPagination(response.data.pagination)
setTenant(response.data.tenant)
} catch (error) {
toast.error('Failed to load users')
console.error('Error loading users:', error)
// If tenant not found, redirect back
if (error.response?.status === 404) {
navigate('/tenants')
}
} finally {
setLoading(false)
}
}
const handleSearch = (e) => {
setSearchTerm(e.target.value)
}
const handleRoleFilter = (e) => {
setSelectedRole(e.target.value)
}
const handleCreateUser = () => {
setEditingUser(null)
setShowUserModal(true)
}
const handleEditUser = (user) => {
setEditingUser(user)
setShowUserModal(true)
}
const handleSaveUser = async (userData) => {
try {
if (editingUser) {
// Update existing user
await api.put(`/management/tenants/${tenantId}/users/${editingUser.id}`, userData)
toast.success('User updated successfully')
} else {
// Create new user
await api.post(`/management/tenants/${tenantId}/users`, userData)
toast.success('User created successfully')
}
setEditingUser(null)
setShowUserModal(false)
loadUsers() // Reload the list
} catch (error) {
console.error('Error saving user:', error)
const message = error.response?.data?.message || 'Failed to save user'
throw new Error(message)
}
}
const handleDeleteUser = async (user) => {
if (!confirm(`Are you sure you want to delete user "${user.username}"? This action cannot be undone.`)) {
return
}
try {
await api.delete(`/management/tenants/${tenantId}/users/${user.id}`)
toast.success('User deleted successfully')
loadUsers()
} catch (error) {
const message = error.response?.data?.message || 'Failed to delete user'
toast.error(message)
console.error('Error deleting user:', error)
}
}
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 getStatusBadge = (isActive) => {
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
isActive
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{isActive ? 'Active' : 'Inactive'}
</span>
)
}
const handlePageChange = (newOffset) => {
setPagination(prev => ({ ...prev, offset: newOffset }))
loadUsers(newOffset)
}
if (loading && !users.length) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading users...</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={() => navigate('/tenants')}
className="flex items-center text-gray-600 hover:text-gray-900"
>
<ArrowLeftIcon className="w-5 h-5 mr-2" />
Back to Tenants
</button>
<div>
<h1 className="text-2xl font-bold text-gray-900">
Users in {tenant?.name || 'Tenant'}
</h1>
<p className="text-gray-600">
Manage users and their roles within this tenant
</p>
</div>
</div>
<button
onClick={handleCreateUser}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<PlusIcon className="w-4 h-4 mr-2" />
Add User
</button>
</div>
{/* Filters */}
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Search Users
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
value={searchTerm}
onChange={handleSearch}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
placeholder="Search by name, username, or email..."
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Filter by Role
</label>
<select
value={selectedRole}
onChange={handleRoleFilter}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="operator">Operator</option>
<option value="viewer">Viewer</option>
</select>
</div>
<div className="flex items-end">
<div className="text-sm text-gray-500">
{pagination.total} total users
</div>
</div>
</div>
</div>
{/* Users Table */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
{users.length === 0 ? (
<div className="text-center py-12">
<UserIcon 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 || selectedRole
? 'Try adjusting your search filters.'
: 'Get started by adding a user to this tenant.'
}
</p>
{(!searchTerm && !selectedRole) && (
<div className="mt-6">
<button
onClick={handleCreateUser}
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<PlusIcon className="w-4 h-4 mr-2" />
Add First User
</button>
</div>
)}
</div>
) : (
<>
<ul className="divide-y divide-gray-200">
{users.map((user) => (
<li key={user.id}>
<div className="px-4 py-4 flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<UserIcon className="h-5 w-5 text-gray-600" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3">
<p className="text-sm font-medium text-gray-900 truncate">
{user.first_name || user.last_name
? `${user.first_name || ''} ${user.last_name || ''}`.trim()
: user.username
}
</p>
{getRoleBadge(user.role)}
{getStatusBadge(user.is_active)}
</div>
<div className="flex items-center space-x-4 mt-1">
<p className="text-sm text-gray-500">@{user.username}</p>
<p className="text-sm text-gray-500">{user.email}</p>
{user.phone && (
<p className="text-sm text-gray-500">{user.phone}</p>
)}
</div>
<p className="text-xs text-gray-400 mt-1">
Created {new Date(user.created_at).toLocaleDateString()}
{user.last_login && (
<span> Last login {new Date(user.last_login).toLocaleDateString()}</span>
)}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleEditUser(user)}
className="p-2 text-gray-400 hover:text-blue-600"
title="Edit user"
>
<PencilIcon className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteUser(user)}
className="p-2 text-gray-400 hover:text-red-600"
title="Delete user"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</div>
</li>
))}
</ul>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(Math.max(0, pagination.offset - pagination.limit))}
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={() => handlePageChange(pagination.offset + pagination.limit)}
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>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => handlePageChange(Math.max(0, pagination.offset - pagination.limit))}
disabled={pagination.offset === 0}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(pagination.offset + pagination.limit)}
disabled={pagination.offset + pagination.limit >= pagination.total}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
</div>
)}
</>
)}
</div>
{/* User Modal */}
<UserModal
isOpen={showUserModal}
onClose={() => {
setShowUserModal(false)
setEditingUser(null)
}}
onSave={handleSaveUser}
user={editingUser}
tenant={tenant}
title={editingUser ? 'Edit User' : 'Create User'}
/>
</div>
)
}
export default TenantUsersPage

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../services/api'
import toast from 'react-hot-toast'
import TenantModal from '../components/TenantModal'
@@ -7,10 +8,12 @@ import {
PencilIcon,
TrashIcon,
MagnifyingGlassIcon,
BuildingOfficeIcon
BuildingOfficeIcon,
UserGroupIcon
} from '@heroicons/react/24/outline'
const Tenants = () => {
const navigate = useNavigate()
const [tenants, setTenants] = useState([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
@@ -93,6 +96,26 @@ const Tenants = () => {
}
}
const toggleTenantStatus = async (tenant) => {
const action = tenant.is_active ? 'deactivate' : 'activate'
const confirmMessage = tenant.is_active
? `Are you sure you want to deactivate "${tenant.name}"? Users will not be able to access this tenant.`
: `Are you sure you want to activate "${tenant.name}"?`
if (!confirm(confirmMessage)) {
return
}
try {
await api.post(`/management/tenants/${tenant.id}/${action}`)
toast.success(`Tenant ${action}d successfully`)
loadTenants()
} catch (error) {
toast.error(`Failed to ${action} tenant`)
console.error(`Error ${action}ing tenant:`, error)
}
}
const handleEditTenant = async (tenant) => {
try {
// Fetch full tenant details for editing
@@ -136,6 +159,18 @@ const Tenants = () => {
)
}
const getStatusBadge = (isActive) => {
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{isActive ? 'Active' : 'Inactive'}
</span>
)
}
if (loading && tenants.length === 0) {
return (
<div className="flex items-center justify-center h-64">
@@ -200,6 +235,9 @@ const Tenants = () => {
<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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
@@ -236,8 +274,24 @@ const Tenants = () => {
<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">
<button
onClick={() => toggleTenantStatus(tenant)}
className="cursor-pointer"
title={`Click to ${tenant.is_active ? 'deactivate' : 'activate'}`}
>
{getStatusBadge(tenant.is_active)}
</button>
</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={() => navigate(`/tenants/${tenant.id}/users`)}
className="text-green-600 hover:text-green-900 p-1 rounded"
title="Manage Users"
>
<UserGroupIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleEditTenant(tenant)}
className="text-blue-600 hover:text-blue-900 p-1 rounded"