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:
tanyar09
2025-11-21 13:33:13 -05:00
parent e6c66e564e
commit 93cb4eda5b
15 changed files with 1348 additions and 188 deletions
+65
View File
@@ -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}`)
},
}
+10 -1
View File
@@ -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)
}
+4 -4
View File
@@ -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
}
+6 -2
View File
@@ -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
View File
@@ -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>
)
}