Arquitectura del Backend (Spring Boot)
Para construir un sistema de autenticación y gestión de usuarios robusto, estructuraremos el backend en capas claras. En el controlador, emplearemos la inyección de dependencias por constructor y estandarizaremos las respuestas HTTP utilizando un objeto genérico ApiResponse.
package com.edu.portal.controller;
import com.edu.portal.dto.ApiResponse;
import com.edu.portal.entity.Faculty;
import com.edu.portal.entity.Student;
import com.edu.portal.entity.SystemUser;
import com.edu.portal.service.AccountManagementService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/accounts")
public class AccountRestController {
private final AccountManagementService accountService;
public AccountRestController(AccountManagementService accountService) {
this.accountService = accountService;
}
@GetMapping("/authenticate")
public ApiResponse<Integer> authenticateUser(@RequestParam String loginId, @RequestParam String secretKey, @RequestParam String roleType) {
int isValid = accountService.verifyCredentials(loginId, secretKey, roleType);
return ApiResponse.success(isValid);
}
@PostMapping("/students/register")
public ApiResponse<Student> registerStudent(@RequestBody Student studentData) {
accountService.createStudentAccount(studentData);
return ApiResponse.success(studentData);
}
@PostMapping("/users/create")
public ApiResponse<SystemUser> createSystemUser(@RequestBody SystemUser sysUser) {
accountService.addSystemUser(sysUser);
return ApiResponse.success(sysUser);
}
@GetMapping("/users/{loginId}")
public ApiResponse<SystemUser> fetchUserDetails(@PathVariable String loginId) {
SystemUser user = accountService.findUserByLoginId(loginId);
return user != null ? ApiResponse.success(user) : ApiResponse.error("User not found");
}
@GetMapping("/students/profile/{loginId}")
public ApiResponse<Student> fetchStudentProfile(@PathVariable String loginId) {
Student student = accountService.findStudentProfile(loginId);
return student != null ? ApiResponse.success(student) : ApiResponse.error("Profile not found");
}
@PutMapping("/users/change-password/{loginId}/{newSecret}")
public ApiResponse<SystemUser> changePassword(@PathVariable String loginId, @PathVariable String newSecret) {
accountService.updateSecretKey(loginId, newSecret);
SystemUser updatedUser = accountService.findUserByLoginId(loginId);
return updatedUser != null ? ApiResponse.success(updatedUser) : ApiResponse.error("Update failed");
}
@GetMapping("/students/pending-approvals")
public ApiResponse<List<Student>> getPendingRegistrations() {
List<Student> pendingStudents = accountService.fetchUnapprovedStudents();
return pendingStudents.isEmpty() ? ApiResponse.error("No pending approvals") : ApiResponse.success(pendingStudents);
}
@PutMapping("/students/approve/{studentId}/{approvalStatus}")
public ApiResponse<List<Student>> updateApprovalStatus(@PathVariable String studentId, @PathVariable int approvalStatus) {
accountService.modifyApprovalStatus(studentId, approvalStatus);
return ApiResponse.success(accountService.fetchUnapprovedStudents());
}
}
La capa de servicio encapsula la lógica de negocio, desacoplando el controlador de la persistencia de datos. Aquí gestionamos la transformación de roles y la delegación de consultas a la base de datos.
package com.edu.portal.service;
import com.edu.portal.mapper.AccountMapper;
import com.edu.portal.entity.Student;
import com.edu.portal.entity.SystemUser;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AccountManagementService {
private final AccountMapper accountMapper;
public AccountManagementService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
public int verifyCredentials(String loginId, String secretKey, String roleType) {
return accountMapper.checkCredentials(loginId, secretKey, roleType);
}
public void createStudentAccount(Student s) {
accountMapper.insertStudent(s.getStudentId(), s.getFullName(), s.getAcademicYear(),
s.getGender(), s.getDepartment(), s.getMajor(), s.getContactNumber(), s.getRole(), s.getApprovalStatus());
}
public void addSystemUser(SystemUser user) {
String rolesCsv = String.join(",", user.getAssignedRoles());
accountMapper.insertSystemUser(user.getLoginId(), user.getSecretKey(), rolesCsv);
}
public Student findStudentProfile(String loginId) {
return accountMapper.selectStudentProfile(loginId);
}
public SystemUser findUserByLoginId(String loginId) {
return accountMapper.selectSystemUser(loginId);
}
public void updateSecretKey(String loginId, String newSecret) {
accountMapper.updatePassword(loginId, newSecret);
}
public List<Student> fetchUnapprovedStudents() {
return accountMapper.selectPendingStudents();
}
public void modifyApprovalStatus(String studentId, int status) {
accountMapper.updateStudentStatus(studentId, status);
}
}
Para la persistencia, empleamos MyBatis con anotaciones, lo que nos permite mapear consultas SQL directamente en interfaces Java. Se han optimizado las consultas para validar credenciales y gestionar estados de aprobación de manera eficiente.
package com.edu.portal.mapper;
import com.edu.portal.entity.Student;
import com.edu.portal.entity.SystemUser;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AccountMapper {
@Select("SELECT COUNT(*) FROM system_users WHERE login_id = #{loginId} AND secret_key = #{secretKey} AND assigned_roles LIKE CONCAT('%', #{roleType}, '%')")
int checkCredentials(@Param("loginId") String loginId, @Param("secretKey") String secretKey, @Param("roleType") String roleType);
@Insert("INSERT INTO students (student_id, full_name, academic_year, gender, department, major, contact_number, role, approval_status) " +
"VALUES (#{studentId}, #{fullName}, #{academicYear}, #{gender}, #{department}, #{major}, #{contactNumber}, #{role}, #{approvalStatus})")
void insertStudent(@Param("studentId") String studentId, @Param("fullName") String fullName, @Param("academicYear") String academicYear,
@Param("gender") String gender, @Param("department") String department, @Param("major") String major,
@Param("contactNumber") String contactNumber, @Param("role") String role, @Param("approvalStatus") int approvalStatus);
@Select("SELECT * FROM students WHERE student_id = #{loginId}")
Student selectStudentProfile(@Param("loginId") String loginId);
@Select("SELECT * FROM system_users WHERE login_id = #{loginId}")
SystemUser selectSystemUser(@Param("loginId") String loginId);
@Update("UPDATE system_users SET secret_key = #{newSecret} WHERE login_id = #{loginId}")
void updatePassword(@Param("loginId") String loginId, @Param("newSecret") String newSecret);
@Select("SELECT * FROM students WHERE approval_status = 0")
List<Student> selectPendingStudents();
@Update("UPDATE students SET approval_status = #{status} WHERE student_id = #{studentId}")
void updateStudentStatus(@Param("studentId") String studentId, @Param("status") int status);
@Insert("INSERT INTO system_users (login_id, secret_key, assigned_roles) VALUES (#{loginId}, #{secretKey}, #{rolesCsv})")
void insertSystemUser(@Param("loginId") String loginId, @Param("secretKey") String secretKey, @Param("rolesCsv") String rolesCsv);
}
Interfaz de Usuario (Frontend)
La interfaz se ha modernizado utilizando async/await para las peticiones fetch, mejorando la legibilidad del código y el manejo de errores frente al uso de promesas encadenadas tradicionales.
Portal de Autenticación
El punto de entrada valida las credenciales contra el backend y redirige dinámicamente al usuario a su panel correspondiente según su rol asignado.
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Portal de Autenticación</title>
<style>
body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f4f7f6; margin: 0; }
.auth-card { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 300px; }
.auth-card h2 { text-align: center; margin-bottom: 1.5rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
.form-group input, .form-group select { width: 100%; padding: 0.5rem; box-sizing: border-box; }
.btn { width: 100%; padding: 0.75rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-top: 0.5rem; }
.btn-primary { background: #007bff; color: #fff; }
.btn-secondary { background: #6c757d; color: #fff; }
</style>
</head>
<body>
<div class="auth-card">
<h2>Inicio de Sesión</h2>
<form id="authForm">
<div class="form-group">
<label for="loginId">Usuario:</label>
<input type="text" id="loginId" required>
</div>
<div class="form-group">
<label for="secretKey">Contraseña:</label>
<input type="password" id="secretKey" required>
</div>
<div class="form-group">
<label for="roleType">Rol:</label>
<select id="roleType" required>
<option value="Estudiante">Estudiante</option>
<option value="Docente">Docente</option>
<option value="Coordinador">Coordinador</option>
<option value="Decano">Decano</option>
<option value="Administrador">Administrador</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Ingresar</button>
<button type="button" id="goToRegister" class="btn btn-secondary">Registro de Estudiantes</button>
</form>
</div>
<script>
document.getElementById('goToRegister').addEventListener('click', () => {
window.location.href = '/student-registration.html';
});
document.getElementById('authForm').addEventListener('submit', async (e) => {
e.preventDefault();
const loginId = document.getElementById('loginId').value;
const secretKey = document.getElementById('secretKey').value;
const roleType = document.getElementById('roleType').value;
try {
const response = await fetch(`/api/accounts/authenticate?loginId=${loginId}&secretKey=${secretKey}&roleType=${roleType}`);
const result = await response.json();
if (result.data === 1) {
alert('Autenticación exitosa');
const routes = {
'Administrador': '/admin/dashboard.html',
'Estudiante': '/student/portal.html',
'Docente': '/faculty/portal.html',
'Coordinador': '/coordinator/portal.html',
'Decano': '/dean/portal.html'
};
window.location.href = `${routes[roleType]}?user=${encodeURIComponent(loginId)}`;
} else {
alert('Credenciales inválidas');
}
} catch (error) {
console.error('Error de red:', error);
alert('Error al conectar con el servidor');
}
});
</script>
</body>
</html>
Panel de Administración
Un dashboard centralizado que actúa como hub para las distintas herramientas de gestión, utilizando CSS Grid para una disttribución responsive de los módulos.
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Panel de Administración</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f7f6; margin: 0; padding: 2rem; }
.dashboard { max-width: 800px; margin: 0 auto; background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #333; }
.menu-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-top: 2rem; }
.menu-btn { padding: 1.5rem; text-align: center; background: #007bff; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold; transition: background 0.3s; }
.menu-btn:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="dashboard">
<h1>Panel de Control Administrativo</h1>
<div class="menu-grid">
<a href="review-students.html" class="menu-btn">Aprobar Registros</a>
<a href="add-faculty.html" class="menu-btn">Agregar Docente</a>
<a href="remove-faculty.html" class="menu-btn">Eliminar Docente</a>
<a href="modify-faculty.html" class="menu-btn">Modificar Docente</a>
<a href="search-faculty.html" class="menu-btn">Buscar Docente</a>
<a href="reset-credentials.html" class="menu-btn">Restablecer Contraseñas</a>
</div>
</div>
</body>
</html>
Flujo de Aprobación de Estudiantes
Los administradores pueden revisar las solicitudes de registro pendientes. Al aprobar una solicitud, el sistema ejecuta una segunda petición para crear automáticamente la cuenta de acceso con credenciales por defecto.
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Revisión de Registros</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f7f6; padding: 2rem; }
.container { max-width: 1000px; margin: 0 auto; background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: center; }
th { background: #007bff; color: white; }
.btn-approve { background: #28a745; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
.btn-reject { background: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
</style>
</head>
<body>
<div class="container">
<h2>Solicitudes de Registro Pendientes</h2>
<table id="requestsTable">
<thead>
<tr>
<th>ID</th><th>Nombre</th><th>Año</th><th>Género</th><th>Facultad</th><th>Carrera</th><th>Teléfono</th><th>Acciones</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
const tbody = document.querySelector('#requestsTable tbody');
async function loadPendingRequests() {
try {
const res = await fetch('/api/accounts/students/pending-approvals');
const json = await res.json();
if (json.data && json.data.length > 0) {
renderTable(json.data);
} else {
tbody.innerHTML = '<tr><td colspan="8">No hay solicitudes pendientes.</td></tr>';
}
} catch (err) {
console.error(err);
}
}
function renderTable(students) {
tbody.innerHTML = '';
students.forEach(s => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${s.studentId}</td><td>${s.fullName}</td><td>${s.academicYear}</td>
<td>${s.gender}</td><td>${s.department}</td><td>${s.major}</td><td>${s.contactNumber}</td>
<td>
<button class="btn-approve" onclick="processApproval('${s.studentId}', 1)">Aprobar</button>
<button class="btn-reject" onclick="processApproval('${s.studentId}', -1)">Rechazar</button>
</td>
`;
tbody.appendChild(tr);
});
}
async function processApproval(studentId, status) {
try {
const res = await fetch(`/api/accounts/students/approve/${studentId}/${status}`, { method: 'PUT' });
const json = await res.json();
alert(status === 1 ? 'Estudiante aprobado y cuenta creada.' : 'Solicitud rechazada.');
if (status === 1) {
await fetch('/api/accounts/users/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ loginId: studentId, secretKey: '123456', assignedRoles: ['Estudiante'] })
});
}
loadPendingRequests();
} catch (err) {
alert('Error al procesar la solicitud.');
}
}
loadPendingRequests();
</script>
</body>
</html>
Registro de Personal Docente
Formulario para dar de alta nuevos docentes. El script del cliente mapea códigos de rol numéricos a listas de permisos específicos antes de enviar los datos estructurados al servidor.
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Registro de Docentes</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f7f6; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.form-card { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 400px; }
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
input { width: 100%; padding: 0.5rem; box-sizing: border-box; }
.btn { width: 100%; padding: 0.75rem; background: #28a745; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }
</style>
</head>
<body>
<div class="form-card">
<h2>Agregar Nuevo Docente</h2>
<form id="facultyForm">
<div class="form-group"><label>ID de Empleado:</label><input type="text" id="empId" required></div>
<div class="form-group"><label>Nombre Completo:</label><input type="text" id="empName" required></div>
<div class="form-group"><label>Facultad:</label><input type="text" id="empDept" required></div>
<div class="form-group"><label>Especialidad:</label><input type="text" id="empMajor" required></div>
<div class="form-group">
<label>Código de Rol (111: Todos, 001: Docente, etc.):</label>
<input type="text" id="empRoleCode" required>
</div>
<button type="submit" class="btn">Registrar Docente</button>
</form>
</div>
<script>
const roleMapping = {
'111': ["Docente", "Coordinador", "Decano"],
'011': ["Docente", "Coordinador"],
'001': ["Docente"],
'100': ["Decano"],
'010': ["Coordinador"],
'110': ["Coordinador", "Decano"],
'101': ["Docente", "Decano"]
};
document.getElementById('facultyForm').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('empId').value;
const name = document.getElementById('empName').value;
const dept = document.getElementById('empDept').value;
const major = document.getElementById('empMajor').value;
const roleCode = document.getElementById('empRoleCode').value;
const roles = roleMapping[roleCode] || ["Docente"];
try {
await fetch('/api/accounts/users/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ loginId: id, secretKey: 'default123', assignedRoles: roles })
});
await fetch('/api/faculty/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employeeId: id, fullName: name, department: dept, major: major, roleCode: roleCode })
});
alert('Docente registrado exitosamente.');
} catch (error) {
alert('Error durante el registro.');
}
});
</script>
</body>
</html>