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
+
+
+
+
+
+
+
+ | Usuario |
+ Rol |
+ Email |
+ Alta |
+ Acciones |
+
+
+
+ {users.map(user => (
+
+ |
+
+
+
+ {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'}
+ >
+
+
+
+ );
+}
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 });
+ }
}