feat: Implement auth user management API and UI for admin users
This commit introduces a new Auth User management feature, allowing admins to create, update, delete, and list users in the auth database. A dedicated API has been implemented with endpoints for managing auth users, including validation for unique email addresses. The frontend has been updated to include a Manage Users page with tabs for backend and frontend users, enhancing the user experience. Additionally, modals for creating and editing auth users have been added, along with appropriate error handling and loading states. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import apiClient from './client'
|
||||
|
||||
export interface AuthUserResponse {
|
||||
id: number
|
||||
name: string | null
|
||||
email: string
|
||||
is_admin: boolean | null
|
||||
has_write_access: boolean | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface AuthUserCreateRequest {
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
is_admin: boolean
|
||||
has_write_access: boolean
|
||||
}
|
||||
|
||||
export interface AuthUserUpdateRequest {
|
||||
email: string
|
||||
name: string
|
||||
is_admin: boolean
|
||||
has_write_access: boolean
|
||||
}
|
||||
|
||||
export interface AuthUsersListResponse {
|
||||
items: AuthUserResponse[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export const authUsersApi = {
|
||||
listUsers: async (): Promise<AuthUsersListResponse> => {
|
||||
const { data } = await apiClient.get<AuthUsersListResponse>('/api/v1/auth-users')
|
||||
return data
|
||||
},
|
||||
|
||||
getUser: async (userId: number): Promise<AuthUserResponse> => {
|
||||
const { data } = await apiClient.get<AuthUserResponse>(`/api/v1/auth-users/${userId}`)
|
||||
return data
|
||||
},
|
||||
|
||||
createUser: async (request: AuthUserCreateRequest): Promise<AuthUserResponse> => {
|
||||
const { data } = await apiClient.post<AuthUserResponse>('/api/v1/auth-users', request)
|
||||
return data
|
||||
},
|
||||
|
||||
updateUser: async (
|
||||
userId: number,
|
||||
request: AuthUserUpdateRequest
|
||||
): Promise<AuthUserResponse> => {
|
||||
const { data } = await apiClient.put<AuthUserResponse>(
|
||||
`/api/v1/auth-users/${userId}`,
|
||||
request
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
||||
deleteUser: async (userId: number): Promise<void> => {
|
||||
await apiClient.delete(`/api/v1/auth-users/${userId}`)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,20 @@ apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Don't redirect if we're already on the login page (prevents clearing error messages)
|
||||
const isLoginPage = window.location.pathname === '/login'
|
||||
|
||||
// Always clear tokens on 401, but only redirect if not already on login page
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
// Clear sessionStorage settings on authentication failure
|
||||
sessionStorage.removeItem('identify_settings')
|
||||
window.location.href = '/login'
|
||||
|
||||
// Only redirect if not already on login page
|
||||
if (!isLoginPage) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
// If on login page, just reject the error so the login component can handle it
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,16 @@ export interface UserResponse {
|
||||
export interface UserCreateRequest {
|
||||
username: string
|
||||
password: string
|
||||
email?: string | null
|
||||
full_name?: string | null
|
||||
email: string
|
||||
full_name: string
|
||||
is_active?: boolean
|
||||
is_admin?: boolean
|
||||
}
|
||||
|
||||
export interface UserUpdateRequest {
|
||||
password?: string | null
|
||||
email?: string | null
|
||||
full_name?: string | null
|
||||
email: string
|
||||
full_name: string
|
||||
is_active?: boolean
|
||||
is_admin?: boolean
|
||||
}
|
||||
|
||||
@@ -11,13 +11,16 @@ export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && isAuthenticated) {
|
||||
// Only redirect if user is already authenticated (e.g., visiting /login while logged in)
|
||||
// Don't redirect on isLoading changes during login attempts
|
||||
if (isAuthenticated && !isLoading) {
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
}, [isAuthenticated, isLoading, navigate])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
@@ -35,7 +38,8 @@ export default function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// Only show loading screen on initial auth check, not during login attempts
|
||||
if (isLoading && !loading) {
|
||||
return <div className="min-h-screen flex items-center justify-center">Loading...</div>
|
||||
}
|
||||
|
||||
|
||||
+599
-159
@@ -1,7 +1,13 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { usersApi, UserResponse, UserCreateRequest, UserUpdateRequest } from '../api/users'
|
||||
import { authUsersApi, AuthUserResponse, AuthUserCreateRequest, AuthUserUpdateRequest } from '../api/authUsers'
|
||||
|
||||
type TabType = 'backend' | 'frontend'
|
||||
|
||||
export default function ManageUsers() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('backend')
|
||||
|
||||
// Backend users state
|
||||
const [users, setUsers] = useState<UserResponse[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -27,9 +33,35 @@ export default function ManageUsers() {
|
||||
is_admin: false,
|
||||
})
|
||||
|
||||
// Frontend users state
|
||||
const [authUsers, setAuthUsers] = useState<AuthUserResponse[]>([])
|
||||
const [authLoading, setAuthLoading] = useState(true)
|
||||
const [authError, setAuthError] = useState<string | null>(null)
|
||||
const [showAuthCreateModal, setShowAuthCreateModal] = useState(false)
|
||||
const [editingAuthUser, setEditingAuthUser] = useState<AuthUserResponse | null>(null)
|
||||
|
||||
const [authCreateForm, setAuthCreateForm] = useState<AuthUserCreateRequest>({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
|
||||
const [authEditForm, setAuthEditForm] = useState<AuthUserUpdateRequest>({
|
||||
email: '',
|
||||
name: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
}, [filterActive, filterAdmin])
|
||||
if (activeTab === 'backend') {
|
||||
loadUsers()
|
||||
} else {
|
||||
loadAuthUsers()
|
||||
}
|
||||
}, [activeTab, filterActive, filterAdmin])
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
@@ -47,6 +79,19 @@ export default function ManageUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
const loadAuthUsers = async () => {
|
||||
try {
|
||||
setAuthLoading(true)
|
||||
setAuthError(null)
|
||||
const response = await authUsersApi.listUsers()
|
||||
setAuthUsers(response.items)
|
||||
} catch (err: any) {
|
||||
setAuthError(err.response?.data?.detail || 'Failed to load auth users')
|
||||
} finally {
|
||||
setAuthLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
@@ -66,11 +111,28 @@ export default function ManageUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthCreate = async () => {
|
||||
try {
|
||||
setAuthError(null)
|
||||
await authUsersApi.createUser(authCreateForm)
|
||||
setShowAuthCreateModal(false)
|
||||
setAuthCreateForm({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
loadAuthUsers()
|
||||
} catch (err: any) {
|
||||
setAuthError(err.response?.data?.detail || 'Failed to create auth user')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!editingUser) return
|
||||
try {
|
||||
setError(null)
|
||||
// Only send password if it's not empty
|
||||
const updateData: UserUpdateRequest = {
|
||||
...editForm,
|
||||
password: editForm.password && editForm.password.trim() !== ''
|
||||
@@ -92,6 +154,30 @@ export default function ManageUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthUpdate = async () => {
|
||||
if (!editingAuthUser) return
|
||||
try {
|
||||
setAuthError(null)
|
||||
const updateData: AuthUserUpdateRequest = {
|
||||
email: authEditForm.email,
|
||||
name: authEditForm.name,
|
||||
is_admin: authEditForm.is_admin,
|
||||
has_write_access: authEditForm.has_write_access,
|
||||
}
|
||||
await authUsersApi.updateUser(editingAuthUser.id, updateData)
|
||||
setEditingAuthUser(null)
|
||||
setAuthEditForm({
|
||||
email: '',
|
||||
name: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
loadAuthUsers()
|
||||
} catch (err: any) {
|
||||
setAuthError(err.response?.data?.detail || 'Failed to update auth user')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (userId: number, username: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||
return
|
||||
@@ -105,6 +191,19 @@ export default function ManageUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthDelete = async (userId: number, name: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user "${name}"?`)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
setAuthError(null)
|
||||
await authUsersApi.deleteUser(userId)
|
||||
loadAuthUsers()
|
||||
} catch (err: any) {
|
||||
setAuthError(err.response?.data?.detail || 'Failed to delete auth user')
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = (user: UserResponse) => {
|
||||
setEditingUser(user)
|
||||
setEditForm({
|
||||
@@ -116,160 +215,306 @@ export default function ManageUsers() {
|
||||
})
|
||||
}
|
||||
|
||||
const startAuthEdit = (user: AuthUserResponse) => {
|
||||
setEditingAuthUser(user)
|
||||
setAuthEditForm({
|
||||
email: user.email || '',
|
||||
name: user.name || '',
|
||||
is_admin: user.is_admin === true,
|
||||
has_write_access: user.has_write_access === true,
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'Never'
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const formatDateShort = (dateString: string | null) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toISOString().split('T')[0] // YYYY-MM-DD format
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Manage Users</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
onClick={() => {
|
||||
if (activeTab === 'backend') {
|
||||
setShowCreateModal(true)
|
||||
} else {
|
||||
setShowAuthCreateModal(true)
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
+ Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
{/* Tabs */}
|
||||
<div className="mb-6 border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('backend')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'backend'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Back end users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('frontend')}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'frontend'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Front End Users
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Error Messages */}
|
||||
{error && activeTab === 'backend' && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<label className="text-sm font-medium text-gray-700">Filters:</label>
|
||||
<select
|
||||
value={filterActive === null ? 'all' : filterActive ? 'active' : 'inactive'}
|
||||
onChange={(e) =>
|
||||
setFilterActive(
|
||||
e.target.value === 'all' ? null : e.target.value === 'active'
|
||||
)
|
||||
}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterAdmin === null ? 'all' : filterAdmin ? 'admin' : 'user'}
|
||||
onChange={(e) =>
|
||||
setFilterAdmin(
|
||||
e.target.value === 'all' ? null : e.target.value === 'admin'
|
||||
)
|
||||
}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
{authError && activeTab === 'frontend' && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{authError}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-gray-500">Loading users...</div>
|
||||
) : users.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">No users found</div>
|
||||
) : (
|
||||
<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">
|
||||
Username
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Email
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Full Name
|
||||
</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">
|
||||
Role
|
||||
</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Login
|
||||
</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">
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{user.username}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.email || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.full_name || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
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">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.is_admin
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{user.is_admin ? 'Admin' : 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(user.created_date)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(user.last_login)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
onClick={() => startEdit(user)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-4"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(user.id, user.username)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
{/* Backend Users Tab */}
|
||||
{activeTab === 'backend' && (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<label className="text-sm font-medium text-gray-700">Filters:</label>
|
||||
<select
|
||||
value={filterActive === null ? 'all' : filterActive ? 'active' : 'inactive'}
|
||||
onChange={(e) =>
|
||||
setFilterActive(
|
||||
e.target.value === 'all' ? null : e.target.value === 'active'
|
||||
)
|
||||
}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterAdmin === null ? 'all' : filterAdmin ? 'admin' : 'user'}
|
||||
onChange={(e) =>
|
||||
setFilterAdmin(
|
||||
e.target.value === 'all' ? null : e.target.value === 'admin'
|
||||
)
|
||||
}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-gray-500">Loading users...</div>
|
||||
) : users.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">No users found</div>
|
||||
) : (
|
||||
<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">
|
||||
Username
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Email
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Full Name
|
||||
</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">
|
||||
Role
|
||||
</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Login
|
||||
</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">
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{user.username}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.email || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.full_name || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
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">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.is_admin
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{user.is_admin ? 'Admin' : 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(user.created_date)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(user.last_login)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
onClick={() => startEdit(user)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-4"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(user.id, user.username)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Frontend Users Tab */}
|
||||
{activeTab === 'frontend' && (
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
{authLoading ? (
|
||||
<div className="p-8 text-center text-gray-500">Loading users...</div>
|
||||
) : authUsers.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">No users found</div>
|
||||
) : (
|
||||
<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">
|
||||
Email
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</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">
|
||||
Write Access
|
||||
</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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{authUsers.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{user.email || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{user.name || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.is_admin === true
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{user.is_admin === true ? 'Admin' : 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.has_write_access === true
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{user.has_write_access === true ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{formatDate(user.created_at)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
onClick={() => startAuthEdit(user)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-4"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAuthDelete(user.id, user.name || user.email)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Modal */}
|
||||
{/* Backend Create Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
@@ -306,7 +551,7 @@ export default function ManageUsers() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -315,11 +560,12 @@ export default function ManageUsers() {
|
||||
setCreateForm({ ...createForm, email: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -328,6 +574,8 @@ export default function ManageUsers() {
|
||||
setCreateForm({ ...createForm, full_name: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
minLength={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -342,17 +590,22 @@ export default function ManageUsers() {
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Active</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={createForm.is_admin}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, is_admin: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Admin</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Role *
|
||||
</label>
|
||||
<select
|
||||
value={createForm.is_admin ? 'admin' : 'user'}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, is_admin: e.target.value === 'admin' })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
@@ -373,7 +626,7 @@ export default function ManageUsers() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Modal */}
|
||||
{/* Backend Edit Modal */}
|
||||
{editingUser && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
@@ -401,7 +654,7 @@ export default function ManageUsers() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -410,11 +663,12 @@ export default function ManageUsers() {
|
||||
setEditForm({ ...editForm, email: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -423,6 +677,8 @@ export default function ManageUsers() {
|
||||
setEditForm({ ...editForm, full_name: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
minLength={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -437,17 +693,22 @@ export default function ManageUsers() {
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Active</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.is_admin ?? false}
|
||||
onChange={(e) =>
|
||||
setEditForm({ ...editForm, is_admin: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Admin</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Role *
|
||||
</label>
|
||||
<select
|
||||
value={editForm.is_admin ?? false ? 'admin' : 'user'}
|
||||
onChange={(e) =>
|
||||
setEditForm({ ...editForm, is_admin: e.target.value === 'admin' })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
@@ -467,7 +728,186 @@ export default function ManageUsers() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frontend Create Modal */}
|
||||
{showAuthCreateModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4">Create New Front End User</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={authCreateForm.email}
|
||||
onChange={(e) =>
|
||||
setAuthCreateForm({ ...authCreateForm, email: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={authCreateForm.name || ''}
|
||||
onChange={(e) =>
|
||||
setAuthCreateForm({ ...authCreateForm, name: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
minLength={1}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password * (min 6 characters)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={authCreateForm.password}
|
||||
onChange={(e) =>
|
||||
setAuthCreateForm({ ...authCreateForm, password: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Role *
|
||||
</label>
|
||||
<select
|
||||
value={authCreateForm.is_admin ? 'admin' : 'user'}
|
||||
onChange={(e) =>
|
||||
setAuthCreateForm({ ...authCreateForm, is_admin: e.target.value === 'admin' })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Write Access *
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={authCreateForm.has_write_access}
|
||||
onChange={(e) =>
|
||||
setAuthCreateForm({ ...authCreateForm, has_write_access: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Grant write access</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowAuthCreateModal(false)}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAuthCreate}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frontend Edit Modal */}
|
||||
{editingAuthUser && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4">
|
||||
Edit Front End User: {editingAuthUser.name || editingAuthUser.email}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={authEditForm.email}
|
||||
onChange={(e) =>
|
||||
setAuthEditForm({ ...authEditForm, email: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={authEditForm.name}
|
||||
onChange={(e) =>
|
||||
setAuthEditForm({ ...authEditForm, name: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={authEditForm.is_admin}
|
||||
onChange={(e) =>
|
||||
setAuthEditForm({ ...authEditForm, is_admin: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Admin</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={authEditForm.has_write_access}
|
||||
onChange={(e) =>
|
||||
setAuthEditForm({ ...authEditForm, has_write_access: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Write Access</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setEditingAuthUser(null)}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAuthUpdate}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user