diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index b5060a8..3492f54 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -4,6 +4,7 @@ import Dashboard from './pages/Dashboard'; import ProtectedLayout from './layouts/ProtectedLayout'; import CategoryManager from './pages/Categories/CategoryManager'; +import UserManager from './pages/Users/UserManager'; function App() { return ( @@ -14,6 +15,7 @@ function App() { }> } /> } /> + } /> diff --git a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx index 82c8740..5e80d7c 100644 --- a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx +++ b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx @@ -24,6 +24,9 @@ export default function ProtectedLayout() {
  • Categorías
  • +
  • + Usuarios +
  • diff --git a/frontend/admin-panel/src/pages/Users/UserManager.tsx b/frontend/admin-panel/src/pages/Users/UserManager.tsx new file mode 100644 index 0000000..5b7d88a --- /dev/null +++ b/frontend/admin-panel/src/pages/Users/UserManager.tsx @@ -0,0 +1,199 @@ +import { useState, useEffect } from 'react'; +// @ts-ignore +import { User } from '../../types/User'; +import { userService } from '../../services/userService'; +import Modal from '../../components/Modal'; +import { Plus, Edit, Trash2, Shield, User as UserIcon } from 'lucide-react'; + +export default function UserManager() { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + + const [formData, setFormData] = useState({ + username: '', + password: '', + role: 'Usuario', + email: '' + }); + + useEffect(() => { + loadUsers(); + }, []); + + const loadUsers = async () => { + setIsLoading(true); + try { + const data = await userService.getAll(); + setUsers(data); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const handleCreate = () => { + setEditingId(null); + setFormData({ username: '', password: '', role: 'Usuario', email: '' }); + setIsModalOpen(true); + }; + + // @ts-ignore + const handleEdit = (user: User) => { + setEditingId(user.id); + // Password left empty intentionally + setFormData({ + username: user.username, + password: '', + role: user.role, + email: user.email || '' + }); + setIsModalOpen(true); + }; + + const handleDelete = async (id: number) => { + if (!confirm('Eliminar usuario?')) return; + try { + await userService.delete(id); + loadUsers(); + } catch (error) { + alert('Error eliminando'); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingId) { + await userService.update(editingId, formData); + } else { + await userService.create(formData as any); + } + setIsModalOpen(false); + loadUsers(); + } catch (error) { + alert('Error guardando usuario'); + } + }; + + return ( +
    +
    +

    Gestión de Usuarios

    + +
    + +
    + + + + + + + + + + + + {users.map(user => ( + + + + + + + + ))} + +
    UsuarioRolEmailAltaAcciones
    +
    + +
    + {user.username} +
    + + + {user.role} + + {user.email || '-'} + {new Date(user.createdAt).toLocaleDateString()} + + + +
    + {users.length === 0 && !isLoading && ( +
    No hay usuarios registrados.
    + )} +
    + + setIsModalOpen(false)} + title={editingId ? 'Editar Usuario' : 'Nuevo Usuario'} + > +
    +
    + + setFormData({ ...formData, username: e.target.value })} + className="w-full border p-2 rounded mt-1" + /> +
    +
    + + setFormData({ ...formData, password: e.target.value })} + className="w-full border p-2 rounded mt-1" + /> +
    +
    + + +
    +
    + + setFormData({ ...formData, email: e.target.value })} + className="w-full border p-2 rounded mt-1" + /> +
    +
    + + +
    +
    +
    +
    + ); +} diff --git a/frontend/admin-panel/src/services/userService.ts b/frontend/admin-panel/src/services/userService.ts new file mode 100644 index 0000000..5bac64f --- /dev/null +++ b/frontend/admin-panel/src/services/userService.ts @@ -0,0 +1,37 @@ +import api from './api'; +// @ts-ignore +import type { User } from '../types/User'; + +interface CreateUserDto { + username: string; + password: string; + role: string; + email?: string; +} + +interface UpdateUserDto { + username: string; + password?: string; + role: string; + email?: string; +} + +export const userService = { + getAll: async (): Promise => { + const response = await api.get('/users'); + // @ts-ignore + return response.data; + }, + + create: async (user: CreateUserDto): Promise => { + await api.post('/users', user); + }, + + update: async (id: number, user: UpdateUserDto): Promise => { + await api.put(`/users/${id}`, user); + }, + + delete: async (id: number): Promise => { + await api.delete(`/users/${id}`); + } +}; diff --git a/frontend/admin-panel/src/types/User.ts b/frontend/admin-panel/src/types/User.ts new file mode 100644 index 0000000..1fc460d --- /dev/null +++ b/frontend/admin-panel/src/types/User.ts @@ -0,0 +1,7 @@ +export interface User { + id: number; + username: string; + role: string; + email: string | null; + createdAt: string; +} diff --git a/src/SIGCM.API/Controllers/UsersController.cs b/src/SIGCM.API/Controllers/UsersController.cs new file mode 100644 index 0000000..bea1596 --- /dev/null +++ b/src/SIGCM.API/Controllers/UsersController.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SIGCM.Application.DTOs; +using SIGCM.Domain.Entities; +using SIGCM.Domain.Interfaces; + +namespace SIGCM.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Roles = "Admin")] // Only admins can manage users +public class UsersController : ControllerBase +{ + private readonly IUserRepository _repository; + + public UsersController(IUserRepository repository) + { + _repository = repository; + } + + [HttpGet] + public async Task GetAll() + { + var users = await _repository.GetAllAsync(); + // Don't expose password hashes + var sanitized = users.Select(u => new { + u.Id, u.Username, u.Role, u.Email, u.CreatedAt + }); + return Ok(sanitized); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var user = await _repository.GetByIdAsync(id); + if (user == null) return NotFound(); + + return Ok(new { user.Id, user.Username, user.Role, user.Email, user.CreatedAt }); + } + + [HttpPost] + public async Task Create(CreateUserDto dto) + { + // Check if exists + var existing = await _repository.GetByUsernameAsync(dto.Username); + if (existing != null) return BadRequest("El nombre de usuario ya existe."); + + var passwordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password); + + var user = new User + { + Username = dto.Username, + PasswordHash = passwordHash, + Role = dto.Role, + Email = dto.Email, + CreatedAt = DateTime.UtcNow + }; + + var id = await _repository.CreateAsync(user); + return CreatedAtAction(nameof(GetById), new { id }, new { id, user.Username }); + } + + [HttpPut("{id}")] + public async Task Update(int id, UpdateUserDto dto) + { + var user = await _repository.GetByIdAsync(id); + if (user == null) return NotFound(); + + user.Username = dto.Username; + user.Role = dto.Role; + user.Email = dto.Email; + + if (!string.IsNullOrEmpty(dto.Password)) + { + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password); + } + + await _repository.UpdateAsync(user); + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + // Safe check: prevent deleting yourself optional but good practice + // For now simple delete + await _repository.DeleteAsync(id); + return NoContent(); + } +} diff --git a/src/SIGCM.Application/DTOs/UserDtos.cs b/src/SIGCM.Application/DTOs/UserDtos.cs new file mode 100644 index 0000000..3af02d6 --- /dev/null +++ b/src/SIGCM.Application/DTOs/UserDtos.cs @@ -0,0 +1,17 @@ +namespace SIGCM.Application.DTOs; + +public class CreateUserDto +{ + public required string Username { get; set; } + public required string Password { get; set; } + public required string Role { get; set; } + public string? Email { get; set; } +} + +public class UpdateUserDto +{ + public required string Username { get; set; } + public string? Password { get; set; } // Opcional, solo si se quiere cambiar + public required string Role { get; set; } + public string? Email { get; set; } +} diff --git a/src/SIGCM.Domain/Interfaces/IUserRepository.cs b/src/SIGCM.Domain/Interfaces/IUserRepository.cs index 27251ce..f22133f 100644 --- a/src/SIGCM.Domain/Interfaces/IUserRepository.cs +++ b/src/SIGCM.Domain/Interfaces/IUserRepository.cs @@ -4,5 +4,9 @@ using SIGCM.Domain.Entities; public interface IUserRepository { Task GetByUsernameAsync(string username); + Task> GetAllAsync(); + Task GetByIdAsync(int id); Task CreateAsync(User user); + Task UpdateAsync(User user); + Task DeleteAsync(int id); } diff --git a/src/SIGCM.Infrastructure/Repositories/UserRepository.cs b/src/SIGCM.Infrastructure/Repositories/UserRepository.cs index 32c5877..1216e31 100644 --- a/src/SIGCM.Infrastructure/Repositories/UserRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/UserRepository.cs @@ -31,4 +31,32 @@ public class UserRepository : IUserRepository SELECT CAST(SCOPE_IDENTITY() as int);"; return await conn.QuerySingleAsync(sql, user); } + + public async Task> GetAllAsync() + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QueryAsync("SELECT Id, Username, Role, Email, CreatedAt, PasswordHash FROM Users"); + } + + public async Task GetByIdAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + return await conn.QuerySingleOrDefaultAsync("SELECT * FROM Users WHERE Id = @Id", new { Id = id }); + } + + public async Task UpdateAsync(User user) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + UPDATE Users + SET Username = @Username, Role = @Role, Email = @Email, PasswordHash = @PasswordHash + WHERE Id = @Id"; + await conn.ExecuteAsync(sql, user); + } + + public async Task DeleteAsync(int id) + { + using var conn = _connectionFactory.CreateConnection(); + await conn.ExecuteAsync("DELETE FROM Users WHERE Id = @Id", new { Id = id }); + } }