SIENEP · del navegador a la base
Registro y gestión del estudiante
Vamos a seguir el recorrido completo del módulo: un funcionario registra un estudiante, abre su ficha, modifica sus datos y cambia su estado. Con nuestro código y explicando qué ocurre en React, en la API y en PostgreSQL.
Requisitos funcionales del recorrido
RF05Alta de EstudiantesRF06Baja Lógica de EstudiantesRF07Modificación de EstudiantesRF08Búsqueda de Estudiantes
Fuente oficial Requerimientos de Programación de UTEC, 2026 · p. 3
Es una guía de estudio. No registra personas, no pide contraseñas y no se conecta a la API. Podés elegir un recorrido y copiar sus fragmentos. El archivo funciona sin internet; al imprimir se incluyen los cuatro recorridos.
Registrar un estudiante
1. El funcionario abre la gestión de estudiantes
https://sienep-pwa.pages.dev/estudiantesEl navegador recibe de Cloudflare Pages el frontend y ejecuta React. main.tsx monta App; App comprueba la configuración y muestra el enrutador. Si la app ya estaba abierta, React Router cambia de pantalla sin volver a descargar toda la aplicación.
Antes de ver esta gestión intervienen la sesión y los permisos. El funcionario debe estar autenticado y autorizado; la ficha de gestión no es la vista propia del estudiante.
2. La ruta de alta exige los permisos del formulario
rutas.tsx · líneas 140–144 · fragmento
element: <RutaConPermiso permiso="estudiantes.crear" lecturas={['estudiantes.leer', 'grupos.leer']} />,
children: [{
path: '/estudiantes/nuevo',
element: <PaginaEstudiantes />,
handle: { titulo: 'Nuevo estudiante', modal: true, fondoModal: '/estudiantes' },estudiantes.crearpermite iniciar el alta.estudiantes.leerygrupos.leerson lecturas que necesita esta pantalla./estudiantes/nuevousaPaginaEstudiantes, que abre el diálogo sobre el listado.handleaporta el título y el fondo modal; no es un permiso del backend.
La pantalla contiene controles de foco y confirmación para descartar cambios sin guardar. Esos controles ayudan a usar el formulario; no reemplazan las validaciones del servidor.
3. El diálogo prepara sus campos y consulta los grupos
DialogoAltaEstudiante.tsx · líneas 93–99 · fragmento
const [cedula, setCedula] = useState('')
const [nombre, setNombre] = useState('')
const [apellido, setApellido] = useState('')
const [fecNacimiento, setFecNacimiento] = useState('')
const [password, setPassword] = useState('')
const [confirmacion, setConfirmacion] = useState('')
const [idsGrupos, setIdsGrupos] = useState<number[]>([])Cada estado contiene lo escrito o seleccionado. idsGrupos mantiene los identificadores de los grupos académicos; la confirmación de contraseña se usa en la pantalla.
DialogoAltaEstudiante.tsx · líneas 335–343 · fragmento
const respuesta = await listarGrupos({ senal: controller.signal })
if (
!montadoRef.current ||
generacionRef.current !== idGeneracionGrupos
) {
return
}
setGrupos(respuesta.filter((g) => g.estActivo))
setCargandoGrupos(false)listarGrupos consulta GET /api/grupos. La pantalla conserva los grupos activos y permite filtrarlos por ITR y carrera. Mientras se cargan, o si la consulta falla, muestra el estado correspondiente.
La señal de AbortController permite cancelar una consulta pendiente. Las comprobaciones de montaje y generación evitan que una respuesta de una apertura anterior actualice el diálogo actual.
4. Al enviar, se comprueban los datos antes del POST
DialogoAltaEstudiante.tsx · líneas 447–454 · fragmento
const manejarEnvio = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
// Guardia sincrónica contra envíos duplicados o concurrentes:
// Solo se permite enviar si no hay una solicitud activa
if (solicitudActivaRef.current !== null) {
return
}DialogoAltaEstudiante.tsx · líneas 458–474 · fragmento
const resultado = validarFormularioAltaEstudiante({
cedula,
nombre,
apellido,
fecNacimiento,
password,
confirmacion,
idsGrupos,
})
if (!resultado.valido) {
setErrores(resultado.errores)
if (resultado.primerCampoInvalido) {
document.getElementById(resultado.primerCampoInvalido)?.focus()
}
return
}- La cédula se comprueba por formato y dígito verificador uruguayo.
- Nombre y apellido deben estar completos.
- La fecha debe ser válida y representar una edad de al menos 18 años.
- Se exige al menos un grupo académico.
- La contraseña inicial debe tener al menos ocho caracteres y su confirmación debe coincidir.
preventDefault() evita un envío HTML tradicional. La guarda de solicitud activa frena un doble clic o un Enter repetido. Si hay errores, React los muestra y lleva el foco al primer campo inválido. Todavía no se crea ningún registro.
La validación del frontend mejora la experiencia. El backend vuelve a comprobar las reglas, incluido el límite de 50 caracteres por nombre y apellido que declara su DTO.
5. Se arma un objeto que coincide con el contrato de alta
DialogoAltaEstudiante.tsx · líneas 490–500 · fragmento
const payload: Readonly<EstudianteCreatePayload> = Object.freeze({
cedula: normalizarCedula(cedula),
nombre: nombre.trim(),
apellido: apellido.trim(),
fecNacimiento,
password,
idsGrupos: [...idsGrupos],
})
setPassword('')
setConfirmacion('')normalizarCedulaquita separadores y prepara la representación de la CI.trim()quita espacios exteriores del nombre y apellido.[...idsGrupos]copia los IDs seleccionados.Object.freezecongela este objeto para el envío; no cifra sus valores.
La contraseña viaja en el payload sobre HTTPS. La pantalla limpia sus campos de contraseña y confirmación antes de esperar la respuesta. La confirmación no se envía: no es parte del DTO.
Tampoco se envían un username, un correo ni un ID de usuario elegidos en el formulario. El backend los genera o determina durante el registro.
6. El cliente HTTP envía el alta y Spring recibe el DTO
estudiantesApi.ts · líneas 300–312 · fragmento
export async function crearEstudiante(
datos: EstudianteCreatePayload,
opciones?: OpcionesApi,
): Promise<EstudianteResponse> {
const respuesta = await clienteHttp.post<unknown>(
'/api/estudiantes',
datos,
{
senal: opciones?.senal,
},
)
return validarEstudianteResponse(respuesta)
}clienteHttp.post usa VITE_API_BASE_URL, serializa el cuerpo a JSON y adjunta la credencial de sesión. En producción la solicitud apunta a la API de Render.
POST https://sienep-api-c8ml.onrender.com/api/estudiantes
Content-Type: application/json
Authorization: Bearer CREDENCIAL_DE_EJEMPLO
{
"cedula": "12345672",
"nombre": "Lucía",
"apellido": "Pereira",
"fecNacimiento": "2003-06-20",
"password": "CLAVE_DE_EJEMPLO",
"idsGrupos": [7]
}EstudianteController.java · líneas 72–79 · fragmento
@PreAuthorize("hasAuthority('estudiantes.crear')")
@PostMapping
public ResponseEntity<EstudianteResponseDto> crear(@Valid @RequestBody EstudianteCreateDto dto) {
return ResponseEntity.status(HttpStatus.CREATED).body(
estudianteService.registrarDto(dto.getCedula(), dto.getNombre(), dto.getApellido(),
dto.getPassword(), dto.getFecNacimiento(), dto.getIdsGrupos())
);
}La ruta de clase es /api/estudiantes. Los filtros autentican la credencial y @PreAuthorize exige el permiso en el servidor. @RequestBody convierte el JSON en EstudianteCreateDto; @Valid aplica sus restricciones. El controlador delega al servicio.
7. El servicio valida la identidad y genera username y correo
EstudianteService.java · líneas 44–54 · fragmento
if (!ValidadorCI.validarCI(cedula))
throw new IllegalArgumentException("CI inválida: " + cedula);
cedula = ValidadorCI.normalizar(cedula);
if (!ValidadorEdad.esMayorDe18(fecNacimiento))
throw new IllegalArgumentException("El estudiante debe ser mayor de 18 años");
if (password == null || password.length() < 8)
throw new IllegalArgumentException("La contraseña debe tener al menos 8 caracteres");
if (usuarioRepository.existsByCedula(cedula))
throw new IllegalArgumentException("Ya existe un usuario con esa CI");
Set<Grupo> grupos = cargarGrupos(idsGrupos);El servicio comprueba la CI, la normaliza, exige al menos 18 años, revisa la longitud mínima de la contraseña y rechaza una CI que ya exista en usuarios. También busca cada grupo por su ID.
EstudianteService.java · líneas 56–63 · fragmento
String baseUsername = nombre.toLowerCase() + "." + apellido.toLowerCase();
String username = baseUsername;
if (usuarioRepository.existsByUsername(username)) {
username = baseUsername + "." + cedula.substring(cedula.length() - 3);
}
String correoBase = baseUsername;
String correo = correoBase + "@estudiantes.utec.edu.uy";El username parte del nombre y apellido en minúsculas. Si ya existe, el código intenta una variante con los últimos tres dígitos de la CI. El correo usa el dominio institucional; ante una coincidencia, el servicio prueba variantes con una letra adicional.
Generar ese texto en la DB no crea un buzón en Google ni demuestra que la persona controle ese correo. Tampoco es una autorización de Calendar o Gmail. La respuesta del servidor es la que confirma qué username y correo quedaron registrados.
8. Se guardan la cuenta, el perfil y las relaciones con grupos
EstudianteService.java · líneas 74–89 · fragmento
Usuario usuario = new Usuario();
usuario.setCedula(cedula);
usuario.setNombre(nombre);
usuario.setApellido(apellido);
usuario.setUsername(username);
usuario.setPassword(passwordEncoder.encode(password));
usuario.setCorreo(correo);
usuario.setFecNacimiento(fecNacimiento);
usuario.setTerminosAceptados(false);
usuario = usuarioRepository.save(usuario);
Estudiante estudiante = new Estudiante();
estudiante.setUsuario(usuario);
estudiante.setGrupos(grupos);
estudiante.setEstActivo(true);
estudiante = estudianteRepository.save(estudiante);Usuariocontiene la identidad, username, correo, fecha de nacimiento y el hash BCrypt de la contraseña.Estudianteagrega el estado del perfil y sus grupos académicos.passwordEncoder.encodeguarda un hash, no la contraseña original.estActivose establece entruedurante el alta;terminosAceptadoscomienza enfalse.
Estudiante.java · líneas 18–29 · fragmento
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "id_usuario")
private Usuario usuario;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "estudiante_grupo",
joinColumns = @JoinColumn(name = "id_estudiante"),
inverseJoinColumns = @JoinColumn(name = "id_grupo")
)
private Set<Grupo> grupos = new HashSet<>();@MapsId hace que el perfil de estudiante comparta el ID de su cuenta en usuarios. @ManyToMany y @JoinTable representan la relación mediante estudiante_grupo.
usuarios estudiantes estudiante_grupo
id_usuario = 501 ───────→ id_usuario = 501 ───────→ id_estudiante = 501
nombre, username, hash est_activo = true id_grupo = 7Spring Data JPA y Hibernate traducen los guardados a SQL. registrarDto es transaccional: la cuenta, el perfil y sus asociaciones forman parte de la operación de alta.
El servicio también solicita una auditoría CREATE. En la copia revisada AuditoriaService.registrar usa REQUIRES_NEW; no se presenta ese registro como si compartiera la transacción de los datos.
9. La API devuelve 201 y React muestra la confirmación
EstudianteResponseDto.from prepara la respuesta con el ID, datos generales, username, correo, fecha, grupos, estado y términos. No incluye la contraseña ni su hash. El controlador responde 201 Created.
DialogoAltaEstudiante.tsx · líneas 522–523 · fragmento
setEstudianteCreado(respuesta)
onEstudianteCreado(respuesta)PaginaEstudiantes.tsx · líneas 336–345 · fragmento
const manejarEstudianteCreado = (nuevo: EstudianteResponse) => {
setEstudiantes((prev) => [
nuevo,
...prev.filter((e) => e.idUsuario !== nuevo.idUsuario),
])
cargarEstudiantes()
setMensajeExito(
`Estudiante ${nuevo.nombre} ${nuevo.apellido} registrado exitosamente.`,
)
}Antes de actualizar la pantalla se valida la forma de la respuesta y se comprueba que siga perteneciendo a la solicitud activa. setEstudianteCreado activa la confirmación del diálogo; el callback informa al listado.
El listado incorpora el resultado y vuelve a consultar el backend. Esa consulta puede traer un conjunto diferente según el alcance del funcionario. No se debe confundir una inserción temporal en el estado de React con el resultado definitivo de lectura del servidor.
10. Alta de cuenta y disponibilidad para seguimiento son cosas distintas
El alta termina con una cuenta y un perfil creados. No inicia sesión automáticamente como el estudiante, no envía su contraseña en la respuesta ni demuestra que ya pueda usarse en todas las pantallas.
EstudianteService.java · líneas 314–316 · fragmento
private boolean esUsable(Estudiante e) {
return e.getEstActivo() && e.getUsuario().getTerminosAceptados();
}La lectura con alcance acotado filtra por estudiante activo y términos aceptados. Por eso una cuenta recién creada, con términos pendientes, puede no aparecer todavía en el listado de un profesional, aunque el alta haya respondido correctamente.
En la copia revisada, el login local marca los términos si estaban pendientes. Ese comportamiento se explica en la guía de inicio de sesión; no equivale a que este formulario de alta presente o recoja un consentimiento nuevo.
“El funcionario abre el alta y el frontend valida los campos y los grupos. Al confirmar, envía un POST con su credencial. El backend exige el permiso, comprueba la CI y la edad, genera la identidad y guarda la cuenta, el perfil y las relaciones con los grupos dentro de la transacción. Devuelve un DTO sin contraseña, y React confirma el registro y recarga el listado.”
Consultar el listado y la ficha
1. La página solicita el listado autorizado
estudiantesApi.ts · líneas 246–253 · fragmento
export async function listarEstudiantes(
opciones?: OpcionesApi,
): Promise<EstudianteResponse[]> {
const respuesta = await clienteHttp.get<unknown>('/api/estudiantes', {
senal: opciones?.senal,
})
return validarListadoEstudiantes(respuesta)
}GET /api/estudiantes requiere sesión y estudiantes.leer. La API toma el ID del solicitante de su autenticación, no de un parámetro que el navegador pueda elegir.
2. El servicio calcula el alcance antes de devolver los datos
EstudianteService.java · líneas 231–242 · fragmento
public List<EstudianteResponseDto> listarTodosDto(Integer idSolicitante, boolean isAdmin) {
Set<Integer> grupos = scopeService.getGruposPermitidos(idSolicitante, isAdmin, "estudiantes.leer");
if (grupos == null) {
return listarTodos().stream().map(EstudianteResponseDto::from).toList();
}
if (grupos.isEmpty()) {
return List.of();
}
return listarPorGrupos(new ArrayList<>(grupos)).stream()
.filter(this::esUsable)
.map(EstudianteResponseDto::from).toList();
}ScopeService obtiene grupos a partir de las asignaciones efectivas del solicitante para el permiso de lectura. Una asignación puede estar referida a un grupo, carrera o ITR.
En esta implementación, null significa alcance de administrador y permite la consulta general. Un conjunto vacío produce una lista vacía. Con grupos acotados, el repositorio busca sus estudiantes y el servicio aplica esUsable antes de formar los DTO.
Esta restricción sucede en el backend. Ocultar una tarjeta con JavaScript no sería suficiente para proteger datos que ya se hubieran enviado.
3. La búsqueda y los filtros reducen el listado en memoria
PaginaEstudiantes.tsx · líneas 180–190 · fragmento
const estudiantesFiltrados = useMemo(() => {
const termino = normalizarTexto(busqueda)
return estudiantes.filter((e) => {
// Filtro de estado
if (filtroEstado === 'activos' && !e.estActivo) {
return false
}
if (filtroEstado === 'inactivos' && e.estActivo) {
return false
}La página filtra el conjunto que recibió. Tiene filtros por estado, ITR, carrera y grupo, y búsqueda textual por nombre, apellido, cédula, username o correo.
Es un filtro local con useMemo y filter: escribir cada letra no hace otro pedido de búsqueda. No amplía el alcance ni permite recuperar estudiantes que el servidor no devolvió.
4. Abrir una ficha hace una nueva consulta por ID
https://sienep-pwa.pages.dev/estudiantes/501estudiantesApi.ts · líneas 258–269 · fragmento
export async function obtenerEstudiante(
id: number,
opciones?: OpcionesApi,
): Promise<EstudianteResponse> {
const respuesta = await clienteHttp.get<unknown>(
`/api/estudiantes/${id}`,
{
senal: opciones?.senal,
},
)
return validarEstudianteResponse(respuesta)
}EstudianteService.java · líneas 249–254 · fragmento
boolean enScope = estudiante.getGrupos().stream()
.anyMatch(g -> grupos.contains(g.getIdGrupo()));
if (!enScope) {
throw new AccessDeniedException("El estudiante no pertenece a tu scope");
}
validarUsable(estudiante);La página usa el ID de la ruta para consultar GET /api/estudiantes/{id}. El servicio busca el registro y, cuando el alcance no es general, exige pertenencia a alguno de los grupos permitidos y disponibilidad del estudiante.
Cambiar el número en la URL no elimina este control. El frontend también comprueba que el idUsuario recibido coincida con el solicitado y descarta respuestas de otra solicitud o de un ciclo de sesión anterior.
5. React presenta la ficha y las acciones permitidas
FichaEstudiante.tsx · líneas 58–61 · fragmento
const estadoTexto = estudiante.estActivo ? 'Activo' : 'Inactivo'
const terminosTexto = estudiante.terminosAceptados
? 'Aceptados'
: 'Pendientes'FichaEstudiante muestra datos generales, correo, grupos y estado. Según permisos y condiciones del perfil, la página ofrece editar, cambiar el estado y navegar a adjuntos, historial o alta de instancia.
La ficha de gestión conecta el seguimiento académico, pero no sustituye los permisos de cada módulo. Crear una instancia desde aquí inicia el flujo que ya explicamos en la web del alta de instancia.
“La API entrega los estudiantes que el funcionario tiene autorizados por sus asignaciones. El navegador permite buscar y filtrar dentro de ese conjunto. Al abrir una ficha, el servidor vuelve a comprobar el alcance por ID, y React presenta los datos y las acciones que corresponden.”
Modificar un estudiante
1. El funcionario abre Editar sobre una ficha
/estudiantes/501/editarrutas.tsx · líneas 148–155 · fragmento
element: <RutaConPermiso permiso="estudiantes.modificar" lecturas={['estudiantes.leer', 'grupos.leer']} />,
children: [{
path: '/estudiantes/:id/editar',
element: <PaginaDetalleEstudiante />,
handle: {
titulo: 'Editar estudiante',
modal: true,
fondoModal: '/estudiantes/:id',La ruta exige modificar y las lecturas que necesita el formulario. DialogoEdicionEstudiante carga el estudiante y los grupos, prepara sus valores iniciales y controla los cambios pendientes.
2. El formulario limita los campos que envía
DialogoEdicionEstudiante.tsx · líneas 615–619 · fragmento
const payload: Readonly<EstudianteUpdatePayload> = Object.freeze({
nombre: nombre.trim(),
apellido: apellido.trim(),
idsGrupos: [...idsGrupos],
})Este diálogo permite modificar nombre, apellido y grupos. El payload no envía cédula, nacimiento, contraseña ni estActivo. La baja y la reactivación tienen su confirmación y sus endpoints separados.
Se valida antes de enviar y se bloquean solicitudes simultáneas. La llamada es PUT /api/estudiantes/{id}, con JSON y la credencial actual.
3. El backend valida permiso y alcance de la modificación
EstudianteService.java · líneas 274–278 · fragmento
verificarAlcance(obtenerPorId(idUsuario), idSolicitante, isAdmin, "estudiantes.modificar");
if (idsGrupos != null && !idsGrupos.isEmpty()) {
verificarGruposEnAlcance(idsGrupos, idSolicitante, isAdmin, "estudiantes.modificar");
}
return EstudianteResponseDto.from(actualizar(idUsuario, nombre, apellido, idsGrupos, estActivo));El controlador exige estudiantes.modificar. El servicio comprueba al estudiante y, si llegan grupos para sustituir la asociación, verifica que todos esos grupos estén en el alcance permitido para esa operación.
Leer una ficha no garantiza poder editarla: cada operación tiene su permiso y su cálculo de alcance. Las restricciones del DTO se aplican otra vez en el servidor.
4. Se actualizan los datos y se recalcula la identidad generada
EstudianteService.java · líneas 122–126 · fragmento
if (nombre != null) usuario.setNombre(nombre);
if (apellido != null) usuario.setApellido(apellido);
if (nombre != null || apellido != null) {
String base = usuario.getNombre().toLowerCase() + "." + usuario.getApellido().toLowerCase();
String nuevoUsername = base;Al recibir nombre o apellido, este servicio vuelve a calcular el username y el correo institucional, con su tratamiento de coincidencias. No se debe describir el cambio como si afectara únicamente al texto visible del nombre.
El estudiante conserva su idUsuario, pero el username que use después para ingresar puede cambiar. Un correo de demostración modificado por fuera del formulario tampoco se considera inmutable: este camino puede regenerarlo.
EstudianteService.java · líneas 144–146 · fragmento
if (idsGrupos != null && !idsGrupos.isEmpty()) {
estudiante.setGrupos(cargarGrupos(idsGrupos));
}La colección de grupos se reemplaza por la selección enviada. El servicio guarda los datos del usuario y del perfil y solicita la auditoría UPDATE. El DTO de respuesta expresa el resultado que debe mostrar la interfaz.
5. La ficha recibe la respuesta y cierra el diálogo
DialogoEdicionEstudiante.tsx · líneas 622–636 · fragmento
const respuesta = await actualizarEstudiante(idObjetivo, payload, {
senal: abortController.signal,
})
if (
!montadoRef.current ||
generacionRef.current !== idGeneracionCapturada ||
solicitudActivaRef.current?.idSolicitud !== idSolicitudActual
) {
return
}
// Éxito: notificar al contenedor para actualizar ficha y listado, y cerrar
onEstudianteActualizado(respuesta)
cerrarDefinitivamente()Después de validar la respuesta y la propiedad de la solicitud, el diálogo llama al callback del contenedor. La página actualiza los datos mostrados y cierra la edición.
No hace falta recargar toda la web para representar el resultado: cambia el estado de React. Si hubo un error, se muestra el mensaje y no se comunica un guardado exitoso.
“El diálogo de edición manda solo nombre, apellido y grupos. El backend verifica el permiso y el alcance, aplica el cambio y devuelve la ficha actualizada. En nuestro servicio también se regeneran username y correo cuando llegan cambios de nombre o apellido, por lo que hay que explicar ese efecto de la edición.”
Cambiar el estado sin borrar el seguimiento
1. Pedir la baja abre una confirmación
La ficha o el listado llama a useCambioEstadoEstudiante. El hook conserva el estudiante objetivo y la acción, y abre DialogoBajaReactivacionEstudiante. Todavía no se escribió nada en la DB.
useCambioEstadoEstudiante.ts · líneas 93–95 · fragmento
setEstudianteParaAccion(estudiante)
setTipoAccion('baja')
setError(null)La confirmación evita confundir el estudiante o ejecutar el cambio con un clic accidental. Al confirmar, una guarda bloquea envíos repetidos.
2. Se envía DELETE, pero la operación es una baja lógica
estudiantesApi.ts · líneas 336–344 · fragmento
export async function darDeBajaEstudiante(
id: number,
opciones?: OpcionesApi,
): Promise<void> {
await clienteHttp.delete(`/api/estudiantes/${id}`, {
esperar: 'vacio',
senal: opciones?.senal,
})
}EstudianteController.java · líneas 104–108 · fragmento
@DeleteMapping("/{id}")
public ResponseEntity<Void> desactivar(@PathVariable Integer id, Authentication auth) {
estudianteService.desactivar(id, (Integer) auth.getPrincipal(), esAdmin(auth));
return ResponseEntity.noContent().build();
}La API exige estudiantes.eliminar y el servicio comprueba el alcance del solicitante. El nombre HTTP DELETE no implica que se borre físicamente la fila: hay que mirar qué hace el servicio.
La respuesta de éxito que construye este controlador es 204 No Content. El cliente espera una respuesta sin cuerpo, no un JSON con la ficha.
3. El servicio desactiva el perfil y revoca sus sesiones locales
EstudianteService.java · líneas 163–174 · fragmento
public void desactivar(Integer idUsuario) {
TransaccionSesiones.conRevocacion(transactionManager, () -> {
Estudiante estudiante = obtenerPorId(idUsuario);
administradorProtection.validarBajaEstudiante(idUsuario);
estudiante.setEstActivo(false);
estudianteRepository.save(estudiante);
sesiones.revocarTodas(idUsuario);
auditoriaService.registrar("estudiantes", "DELETE", String.valueOf(idUsuario), null);
return null;
});
}validarBajaEstudianteaplica la protección administrativa correspondiente.setEstActivo(false)marca el perfil como inactivo.savepersiste ese estado: la cuenta y el historial no se eliminan aquí.sesiones.revocarTodasrevoca las vigencias de los JWT locales de ese usuario.
El cambio de estado y la revocación se ejecutan dentro de la transacción controlada por TransaccionSesiones.conRevocacion. La auditoría se solicita con la acción DELETE, aunque la baja de negocio sea lógica.
La revocación local no equivale a revocar el access token emitido por Auth0. Aun así, al autenticar nuevas peticiones de ese proveedor, la API vuelve a comprobar que la cuenta de SIENEP esté habilitada. Una petición que ya pasó el filtro no se cancela retroactivamente.
4. React representa el nuevo estado después del éxito
useCambioEstadoEstudiante.ts · líneas 170–174 · fragmento
const nuevoEstado = accionActual === 'reactivar'
const estudianteActualizado: EstudianteResponse = {
...estudianteActual,
estActivo: nuevoEstado,
}Como la respuesta de la baja no trae datos, el hook toma el estudiante que tenía y actualiza estActivo solo después de que la solicitud termina correctamente. Luego notifica al contenedor y, donde está configurado, recarga el listado.
El estudiante puede desaparecer del resultado si ya no cumple el filtro de estado o las reglas de lectura del servidor. Si la operación falla, el hook muestra el error y no anuncia una baja exitosa.
5. Reactivar usa otro endpoint y no recupera tokens revocados
estudiantesApi.ts · líneas 350–362 · fragmento
export async function activarEstudiante(
id: number,
opciones?: OpcionesApi,
): Promise<void> {
await clienteHttp.post(
`/api/estudiantes/${id}/activar`,
undefined,
{
esperar: 'vacio',
senal: opciones?.senal,
},
)
}EstudianteService.java · líneas 177–183 · fragmento
public Estudiante activar(Integer idUsuario) {
Estudiante estudiante = obtenerPorId(idUsuario);
estudiante.setEstActivo(true);
estudianteRepository.save(estudiante);
auditoriaService.registrar("estudiantes", "ACTIVATE", String.valueOf(idUsuario), null);
return estudiante;
}La reactivación utiliza POST /api/estudiantes/{id}/activar, exige estudiantes.modificar y comprueba el alcance. Guarda estActivo=true y el controlador devuelve 204.
Se mantiene el mismo ID, la cuenta y sus relaciones. React actualiza la presentación tras el éxito. Las filas de sesión revocadas no se recrean al reactivar: el estudiante necesita iniciar una sesión nueva.
“La baja exige una confirmación y una autorización en el servidor. Aunque se usa DELETE, se marca el perfil como inactivo, se conservan los datos y se revocan sus sesiones locales. React muestra el cambio después de recibir éxito. La reactivación vuelve a habilitar el perfil por otro endpoint, sin resucitar tokens que ya se habían revocado.”
La idea que conecta todo el módulo
La cuenta de acceso vive en usuarios; el perfil académico y su estado, en estudiantes; las pertenencias, en estudiante_grupo. React mantiene la pantalla, la API verifica las operaciones y PostgreSQL conserva los datos.
Autenticación, permiso y alcance responden preguntas distintas: quién solicita, qué operación puede hacer y sobre qué estudiantes o grupos. Un filtro del listado no amplía lo que autorizó el backend.
La respuesta del servidor conduce el cambio visible de la interfaz. Un formulario enviado, una tarjeta escondida o un botón deshabilitado no acreditan por sí solos que se haya guardado, eliminado o protegido un registro.
Archivos revisados y alcance de la explicación
Guía elaborada el 09/11/2026 leyendo las copias conservadas en el paquete del 06/11/2026. Los bloques indican sus archivos y líneas. Los ejemplos HTTP son ficticios y no se enviaron a la API. Este documento no ejecuta una nueva QA ni certifica una versión distinta en producción.
frontend/src/aplicacion/rutas.tsxfrontend/src/modulos/estudiantes/componentes/DialogoAltaEstudiante.tsxfrontend/src/modulos/estudiantes/api/estudiantesApi.tsbackend/src/main/java/edu/utec/sienep/controller/EstudianteController.javabackend/src/main/java/edu/utec/sienep/service/EstudianteService.javabackend/src/main/java/edu/utec/sienep/entity/Estudiante.javafrontend/src/modulos/estudiantes/paginas/PaginaEstudiantes.tsxfrontend/src/modulos/estudiantes/componentes/FichaEstudiante.tsxfrontend/src/modulos/estudiantes/componentes/DialogoEdicionEstudiante.tsxfrontend/src/modulos/estudiantes/hooks/useCambioEstadoEstudiante.tsbackend/src/main/java/edu/utec/sienep/dto/EstudianteCreateDto.javabackend/src/main/java/edu/utec/sienep/dto/EstudianteUpdateDto.javabackend/src/main/java/edu/utec/sienep/dto/EstudianteResponseDto.javabackend/src/main/java/edu/utec/sienep/service/ScopeService.javabackend/src/main/java/edu/utec/sienep/service/AuditoriaService.javabackend/src/main/java/edu/utec/sienep/service/SesionTokenService.javabackend/src/main/java/edu/utec/sienep/service/LoginService.javafrontend/src/modulos/estudiantes/validacion.ts