src/actions/authentication.jsimport { getServiceURL, handleHTTPErrors } from '../utilities/rest';export const USER_LOGIN_BEGIN = 'USER_LOGIN_BEGIN'; export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'; export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
Tres constantes de cadena exportadas para uso por el reducer (reducers/authentication.js). El switch(type) del reducer coincide con estas cadenas exactas.
¿Por qué cadenas y no símbolos? Convención de Redux: las cadenas son serializables, depurables en Redux DevTools y funcionan entre módulos sin necesidad de referencias compartidas.
Nota: USER_LOGOUT estaba aquí anteriormente: se eliminó en el commit 7c60c2fbb (jul 2019) cuando el cierre de sesión se simplificó para simplemente enviar USER_LOGIN_BEGIN (reinicia el estado).
export const userLoginBegin = () => ({
type: USER_LOGIN_BEGIN,
});
export const userLoginSuccess = (user, version, buildTimestamp, alertMessage) => ({
type: USER_LOGIN_SUCCESS,
payload: { user, version, buildTimestamp, alertMessage },
});
export const userLoginFailure = (error) => ({
type: USER_LOGIN_FAILURE,
payload: { error },
});Creadores de acción — funciones puras que devuelven objetos planos. Los «mensajes» síncronos enviados al reducer.
| Creador | Firma | Qué hace el reducer con ello |
|---|---|---|
userLoginBegin() |
() → { type: BEGIN } |
Reinicia a { loading: true, error: null, user: null } — activa el spinner, borra datos antiguos |
userLoginSuccess(user, version, buildTimestamp, alertMessage) |
(obj, str, str, str?) → { type, payload } |
Escribe los cuatro campos en el estado: user, version, buildTimestamp, alertMessage. loading → false |
userLoginFailure(error) |
(str) → { type, payload: { error } } |
Almacena el mensaje de error, borra user, loading → false |
Estas funciones se llaman desde los thunks siguientes; nunca se envían directamente desde los componentes.
const catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message));
Función curry: catchError(dispatch)(error).
dispatch: la función de envío del store de Redux, inyectada por el middleware de thunk.error: su propiedad .message se convierte en el payload.Se usa en las cadenas .catch() de login() y loadUserStatus():
login() en la línea 84: .catch(catchError(dispatch))loadUserStatus() en la línea 61: catchError(dispatch)({ message: undefined }) — pasa { message: undefined } porque loadUserStatus maneja el error de forma distinta (redirige a la página de inicio de sesión y no requiere un mensaje de error).export const loadUserStatus = () => (dispatch) => {
dispatch(userLoginBegin()); // (a)
return fetch( // (b)
getServiceURL('userStatus'),
{ method: 'GET', credentials: 'include' },
)
.then(handleHTTPErrors) // (c)
.then((response) => response.json()) // (d)
.then(({ userData, systemData, alertMessage }) => { // (e)
dispatch(userLoginSuccess(
userData,
systemData.version,
systemData.buildTimestamp,
alertMessage,
));
})
.catch(() => { // (f)
const { pathname, search } = window.location;
const href = pathname + search;
if (!pathname.startsWith('/react/public/login')
&& !pathname.startsWith('/react/public/datatransfer/')) {
window.location.href =
`/react/public/login?url=${encodeURIComponent(href)}`;
}
catchError(dispatch)({ message: undefined });
});
};Propósito: «¿Quién soy?» — se ejecuta al inicio de la app y tras el inicio de sesión para comprobar si el usuario tiene una sesión válida.
(a) Envía USER_LOGIN_BEGIN: activa el spinner y borra el estado anterior.
(b) GET /rs/userStatus: envía la cookie de sesión (credentials: 'include'). Llama a UserStatusRest.kt:111.
(c) handleHTTPErrors — si status ≠ 2xx, lanza un Error('Fetch failed: Error {status}'). El flujo pasa al .catch().
(d) Analiza la respuesta JSON. Estructura esperada: { userData: {...}, systemData: {version, buildTimestamp}, alertMessage?: string }.
(e) Desestructura y envía USER_LOGIN_SUCCESS con todos los campos. El reducer los escribe en el estado.
(f) Sesión inválida o expirada. Dos casos:
handleHTTPErrors lanza una excepción → redirigimos a la página de inicio de sesión, conservando la URL actual como parámetro ?url= para que el usuario vuelva a donde estaba.Envío: catchError(dispatch)({ message: undefined }) — error.message será undefined. El reducer almacena { error: undefined } — borrando efectivamente cualquier error anterior.
Se invoca en:
useEffect(() => dispatch(loadUserStatus()), [])login() llama a loadUserStatus()(dispatch) en la línea 83export const login = (username, password, keepSignedIn) => (dispatch) => {
dispatch(userLoginBegin()); // (a)
return fetch( // (b)
getServiceURL('/rsPublic/login'),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
stayLoggedIn: keepSignedIn, // (c)
}),
credentials: 'include',
},
)
.then(handleHTTPErrors) // (d)
.then(() => loadUserStatus()(dispatch)) // (e)
.catch(catchError(dispatch)); // (f)
};Propósito: Enviar las credenciales al servidor. Si es exitoso, consultar la sesión para obtener los datos del usuario.
(a) Envía USER_LOGIN_BEGIN: activa el spinner.
(b) POST /rsPublic/login con cuerpo JSON. Llama a LoginPageRest.kt:93 → LoginService.authenticate(). El servidor establece la cookie JSESSIONID al éxito.
(c) stayLoggedIn: keepSignedIn — indicador de «Recordarme». Cuando es true, el servidor también establece una cookie stayLoggedIn (CookieService.kt:201) válida por 30 días. En la próxima visita, esta cookie renueva automáticamente la sesión sin contraseña.
(d) handleHTTPErrors — si el inicio de sesión falla (401, 403, etc.), lanza una excepción y pasa a .catch().
(e) En caso de éxito (200): encadena la ejecución de loadUserStatus(). El inicio de sesión solo autentica; aún necesitamos saber qué usuario ha entrado. loadUserStatus()(dispatch) invoca el thunk manualmente (devuelve (dispatch) => {...}, por lo que lo llamamos pasando dispatch). Por eso la prueba espera 3 acciones: BEGIN (login) + BEGIN (loadUserStatus) + SUCCESS (loadUserStatus).
(f) En caso de fallo: catchError(dispatch) envía USER_LOGIN_FAILURE con el mensaje de error: por ejemplo 'Fetch failed: Error 401' para contraseña incorrecta.
INICIO DE LA APP:
ProjectForge.jsx → dispatch(loadUserStatus())
└─ GET /rs/userStatus
├─ 200 → SUCCESS(userData, systemData, alertMessage)
└─ 401 → redirige a /react/public/login
INICIO DE SESIÓN:
login(username, password, keepSignedIn)
├─ dispatch(BEGIN)
├─ POST /rsPublic/login
├─ el servidor establece JSESSIONID (+ cookie stayLoggedIn si keepSignedIn)
├─ 200 → loadUserStatus()(dispatch)
│ └─ GET /rs/userStatus → BEGIN → SUCCESS
└─ 401/error → catchError → FAILURE
DESPUÉS DEL INICIO DE SESIÓN:
El usuario vuelve a la página original. ProjectForge.jsx se vuelve a montar.
loadUserStatus() se ejecuta de nuevo; esta vez JSESSIONID es válida → SUCCESS
Documentación relacionada: Comentario del diff de authentication.test.js | Comentario del diff de reducers/authentication.js
Dos utilidades de rest.js:
getServiceURL(path)— construye la URL completa: añadehttp://localhost:8080(dev) o""(prod).'/rsPublic/login'→ devhttp://localhost:8080/rsPublic/login.handleHTTPErrors(response)— lanzaError('Fetch failed: Error {status}')si!response.ok.Sin otras dependencias. El archivo es autocontenido salvo por estas dos funciones auxiliares.