510 lines
17 KiB
TypeScript
510 lines
17 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Alert, AlertDescription } from "@/components/ui/alert"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog"
|
|
import { Edit2, Plus, Save, X, Loader2, RefreshCw, Users, Shield, Trash2 } from "lucide-react"
|
|
import { isAdmin, getCurrentUserEmail } from "@/lib/auth"
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
|
|
|
|
interface User {
|
|
_id: {
|
|
$oid: string
|
|
}
|
|
email: string
|
|
roles: string[]
|
|
}
|
|
|
|
interface UsersResponse {
|
|
success: boolean
|
|
data: User[]
|
|
}
|
|
|
|
interface CreateUserRequest {
|
|
email: string
|
|
password: string
|
|
roles: string[]
|
|
}
|
|
|
|
interface UpdateUserRequest {
|
|
email: string
|
|
password?: string
|
|
roles: string[]
|
|
}
|
|
|
|
// Roles disponíveis no sistema
|
|
const AVAILABLE_ROLES = [
|
|
{ id: "admin", label: "Administrador", description: "Acesso total ao sistema" },
|
|
{ id: "user", label: "Usuário", description: "Acesso parcial, não pode gerenciar usuários" },
|
|
]
|
|
|
|
export default function UserManagement() {
|
|
const [users, setUsers] = useState<User[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState("")
|
|
const [success, setSuccess] = useState("")
|
|
|
|
// Estados para criação de usuário
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
|
const [isCreating, setIsCreating] = useState(false)
|
|
const [createForm, setCreateForm] = useState<CreateUserRequest>({
|
|
email: "",
|
|
password: "",
|
|
roles: [],
|
|
})
|
|
|
|
// Estados para edição de usuário
|
|
const [editingUser, setEditingUser] = useState<string | null>(null)
|
|
const [editForm, setEditForm] = useState<UpdateUserRequest>({
|
|
email: "",
|
|
password: "",
|
|
roles: [],
|
|
})
|
|
const [isUpdating, setIsUpdating] = useState(false)
|
|
|
|
// Estados para exclusão
|
|
const [isDeleting, setIsDeleting] = useState<string | null>(null)
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem("access_token")
|
|
return {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
}
|
|
}
|
|
|
|
const fetchUsers = async () => {
|
|
setIsLoading(true)
|
|
setError("")
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/users`, {
|
|
method: "GET",
|
|
headers: getAuthHeaders(),
|
|
})
|
|
|
|
if (response.ok) {
|
|
const result: UsersResponse = await response.json()
|
|
setUsers(result.data || [])
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao buscar usuários")
|
|
}
|
|
} catch (err: unknown) {
|
|
console.log(err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const createUser = async () => {
|
|
if (!createForm.email || !createForm.password || createForm.roles.length === 0) {
|
|
setError("Preencha todos os campos obrigatórios")
|
|
return
|
|
}
|
|
|
|
setIsCreating(true)
|
|
setError("")
|
|
setSuccess("")
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/auth/signup`, {
|
|
method: "POST",
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(createForm),
|
|
})
|
|
|
|
if (response.ok) {
|
|
setSuccess("Usuário criado com sucesso!")
|
|
setCreateForm({ email: "", password: "", roles: [] })
|
|
setIsCreateDialogOpen(false)
|
|
await fetchUsers()
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao criar usuário")
|
|
}
|
|
} catch (err:unknown) {
|
|
console.log(err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsCreating(false)
|
|
}
|
|
}
|
|
|
|
const updateUser = async (userId: string) => {
|
|
if (!editForm.email || editForm.roles.length === 0) {
|
|
setError("Preencha todos os campos obrigatórios")
|
|
return
|
|
}
|
|
|
|
setIsUpdating(true)
|
|
setError("")
|
|
setSuccess("")
|
|
|
|
try {
|
|
const updateData: UpdateUserRequest = {
|
|
email: editForm.email,
|
|
roles: editForm.roles,
|
|
}
|
|
|
|
// Só incluir password se foi preenchido
|
|
if (editForm.password && editForm.password.trim() !== "") {
|
|
updateData.password = editForm.password
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
|
|
method: "PATCH",
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(updateData),
|
|
})
|
|
|
|
if (response.ok) {
|
|
setSuccess("Usuário atualizado com sucesso!")
|
|
setEditingUser(null)
|
|
setEditForm({ email: "", password: "", roles: [] })
|
|
await fetchUsers()
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao atualizar usuário")
|
|
}
|
|
} catch (err) {
|
|
console.log(err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsUpdating(false)
|
|
}
|
|
}
|
|
|
|
const deleteUser = async (userId: string) => {
|
|
if (!confirm("Tem certeza que deseja excluir este usuário? Esta ação não pode ser desfeita.")) {
|
|
return
|
|
}
|
|
|
|
setIsDeleting(userId)
|
|
setError("")
|
|
setSuccess("")
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
|
|
method: "DELETE",
|
|
headers: getAuthHeaders(),
|
|
})
|
|
|
|
if (response.ok) {
|
|
setSuccess("Usuário excluído com sucesso!")
|
|
await fetchUsers()
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao excluir usuário")
|
|
}
|
|
} catch (err:unknown) {
|
|
console.log(err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsDeleting(null)
|
|
}
|
|
}
|
|
|
|
const startEditUser = (user: User) => {
|
|
setEditingUser(user._id.$oid)
|
|
setEditForm({
|
|
email: user.email,
|
|
password: "", // Deixar vazio para não alterar a senha
|
|
roles: [...user.roles],
|
|
})
|
|
}
|
|
|
|
const cancelEditUser = () => {
|
|
setEditingUser(null)
|
|
setEditForm({ email: "", password: "", roles: [] })
|
|
}
|
|
|
|
const handleEditRoleChange = (roleId: string, checked: boolean | "indeterminate") => {
|
|
if (checked === true) {
|
|
setEditForm((prev) => ({ ...prev, roles: [...prev.roles, roleId] }))
|
|
} else {
|
|
setEditForm((prev) => ({ ...prev, roles: prev.roles.filter((role) => role !== roleId) }))
|
|
}
|
|
}
|
|
|
|
const handleCreateRoleChange = (roleId: string, checked: boolean | "indeterminate") => {
|
|
if (checked === true) {
|
|
setCreateForm((prev) => ({ ...prev, roles: [...prev.roles, roleId] }))
|
|
} else {
|
|
setCreateForm((prev) => ({ ...prev, roles: prev.roles.filter((role) => role !== roleId) }))
|
|
}
|
|
}
|
|
|
|
const getRoleBadgeVariant = (role: string) => {
|
|
switch (role) {
|
|
case "admin":
|
|
return "destructive" as const
|
|
case "financeiro":
|
|
return "default" as const
|
|
case "user":
|
|
return "secondary" as const
|
|
default:
|
|
return "outline" as const
|
|
}
|
|
}
|
|
|
|
const getRoleLabel = (roleId: string) => {
|
|
const role = AVAILABLE_ROLES.find((r) => r.id === roleId)
|
|
return role ? role.label : roleId
|
|
}
|
|
|
|
const canDeleteUser = (user: User) => {
|
|
const currentUserEmail = getCurrentUserEmail()
|
|
// Não permitir que o usuário delete a si mesmo
|
|
return user.email !== currentUserEmail
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchUsers()
|
|
}, [])
|
|
|
|
// Verificar se o usuário tem permissão para gerenciar usuários
|
|
if (!isAdmin()) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="rounded-full bg-red-100 p-6 mb-4">
|
|
<Shield className="h-8 w-8 text-red-600" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2">Acesso Negado</h3>
|
|
<p className="text-gray-500 max-w-md">
|
|
Você não tem permissão para acessar o gerenciamento de usuários. Entre em contato com um administrador.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Cabeçalho com botão de criar */}
|
|
<div className="flex justify-end items-center">
|
|
<div className="flex gap-2">
|
|
<Button onClick={fetchUsers} disabled={isLoading} variant="outline">
|
|
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
|
Atualizar
|
|
</Button>
|
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Novo Usuário
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Criar Novo Usuário</DialogTitle>
|
|
<DialogDescription>Preencha as informações do novo usuário e suas permissões</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email *</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={createForm.email}
|
|
onChange={(e) => setCreateForm((prev) => ({ ...prev, email: e.target.value }))}
|
|
placeholder="usuario@exemplo.com"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Senha *</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
value={createForm.password}
|
|
onChange={(e) => setCreateForm((prev) => ({ ...prev, password: e.target.value }))}
|
|
placeholder="Senha do usuário"
|
|
minLength={8}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Permissões *</Label>
|
|
<div className="space-y-2">
|
|
{AVAILABLE_ROLES.map((role) => (
|
|
<div key={role.id} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={`create-${role.id}`}
|
|
checked={createForm.roles.includes(role.id)}
|
|
onCheckedChange={(checked) => handleCreateRoleChange(role.id, checked as boolean)}
|
|
/>
|
|
<div className="flex-1">
|
|
<Label htmlFor={`create-${role.id}`} className="text-sm font-medium">
|
|
{role.label}
|
|
</Label>
|
|
<p className="text-xs text-gray-500">{role.description}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 pt-4">
|
|
<Button onClick={createUser} disabled={isCreating} className="flex-1">
|
|
{isCreating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
|
Criar Usuário
|
|
</Button>
|
|
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} disabled={isCreating}>
|
|
Cancelar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{success && (
|
|
<Alert>
|
|
<AlertDescription>{success}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Tabela de Usuários */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="h-5 w-5" />
|
|
Lista de Usuários ({users.length} usuários)
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{users.length > 0 ? (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Permissões</TableHead>
|
|
<TableHead>Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.map((user) => (
|
|
<TableRow key={user._id.$oid}>
|
|
<TableCell>
|
|
{editingUser === user._id.$oid ? (
|
|
<Input
|
|
type="email"
|
|
value={editForm.email}
|
|
onChange={(e) => setEditForm((prev) => ({ ...prev, email: e.target.value }))}
|
|
placeholder="Email do usuário"
|
|
/>
|
|
) : (
|
|
<div className="font-medium">{user.email}</div>
|
|
)}
|
|
{editingUser === user._id.$oid && (
|
|
<div className="mt-2">
|
|
<Input
|
|
type="password"
|
|
value={editForm.password}
|
|
onChange={(e) => setEditForm((prev) => ({ ...prev, password: e.target.value }))}
|
|
placeholder="Nova senha (deixe vazio para não alterar)"
|
|
/>
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{editingUser === user._id.$oid ? (
|
|
<div className="space-y-2">
|
|
{AVAILABLE_ROLES.map((role) => (
|
|
<div key={role.id} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={`edit-${role.id}-${user._id.$oid}`}
|
|
checked={editForm.roles.includes(role.id)}
|
|
onCheckedChange={(checked) => handleEditRoleChange(role.id, checked as boolean)}
|
|
/>
|
|
<Label htmlFor={`edit-${role.id}-${user._id.$oid}`} className="text-sm">
|
|
{role.label}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-wrap gap-1">
|
|
{user.roles.map((role) => (
|
|
<Badge key={role} variant={getRoleBadgeVariant(role)}>
|
|
{getRoleLabel(role)}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex gap-2">
|
|
{editingUser === user._id.$oid ? (
|
|
<>
|
|
<Button size="sm" onClick={() => updateUser(user._id.$oid)} disabled={isUpdating}>
|
|
{isUpdating ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Save className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={cancelEditUser} disabled={isUpdating}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button size="sm" variant="outline" onClick={() => startEditUser(user)}>
|
|
<Edit2 className="h-4 w-4" />
|
|
</Button>
|
|
{canDeleteUser(user) && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => deleteUser(user._id.$oid)}
|
|
disabled={isDeleting === user._id.$oid}
|
|
>
|
|
{isDeleting === user._id.$oid ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Trash2 className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
{isLoading ? "Carregando..." : "Nenhum usuário encontrado."}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|