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';
Three string constants exported for use by the reducer (reducers/authentication.js). The reducer's switch(type) matches against these exact strings.
Why strings, not symbols? Redux convention — strings are serialisable, debuggable in Redux DevTools, and work across module boundaries without shared references.
Note: USER_LOGOUT used to be here — removed in commit 7c60c2fbb (Jul 2019) when logout was simplified to just dispatch USER_LOGIN_BEGIN (resets state).
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 },
});Action creators — pure functions returning plain objects. The synchronous "messages" sent to the reducer.
| Creator | Signature | What the reducer does with it |
|---|---|---|
userLoginBegin() |
() → { type: BEGIN } |
Resets to { loading: true, error: null, user: null } — spinner on, old data cleared |
userLoginSuccess(user, version, buildTimestamp, alertMessage) |
(obj, str, str, str?) → { type, payload } |
Writes all four fields to state — user, version, buildTimestamp, alertMessage. loading → false |
userLoginFailure(error) |
(str) → { type, payload: { error } } |
Stores error message, clears user, loading → false |
These are called from the thunks below — never dispatched directly by components.
const catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message));
Curried function: catchError(dispatch)(error).
dispatch — the Redux store's dispatch function, injected by the thunk middleware.error object — its .message property becomes the payload.Used in .catch() chains of both login() and loadUserStatus():
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 });
});
};Purpose: "Who am I?" — called on app startup and after login to check if the user has a valid session.
(a) Dispatches USER_LOGIN_BEGIN — spinner on, old state cleared.
(b) GET /rs/userStatus — sends the session cookie (credentials: 'include'). This hits UserStatusRest.kt:111.
(c) handleHTTPErrors — if status ≠ 2xx, throws Error('Fetch failed: Error {status}'). Falls through to .catch().
(d) Parse JSON response. Expected shape: { userData: {...}, systemData: {version, buildTimestamp}, alertMessage?: string }.
(e) Destructure and dispatch USER_LOGIN_SUCCESS with all fields. The reducer writes them to state.
(f) Session invalid or expired. Two cases:
handleHTTPErrors throws → we redirect to login page, preserving the current URL as ?url= parameter so the user returns where they were.Dispatch: catchError(dispatch)({ message: undefined }) — error.message will be undefined. The reducer stores { error: undefined } — effectively clearing any previous error.
This is called:
useEffect(() => dispatch(loadUserStatus()), [])login() calls loadUserStatus()(dispatch) at line 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)
};Purpose: Send credentials to the server. If successful, query the session to get user data.
(a) Dispatch USER_LOGIN_BEGIN — spinner on.
(b) POST /rsPublic/login with JSON body. Hits LoginPageRest.kt:93 → LoginService.authenticate(). The server sets JSESSIONID cookie on success.
(c) stayLoggedIn: keepSignedIn — the "remember me" flag. When true, the server additionally sets a stayLoggedIn cookie (CookieService.kt:201) valid for 30 days. On next visit, this cookie auto-renews the session without password.
(d) handleHTTPErrors — if login fails (401, 403, etc.), throws and falls to .catch().
(e) On success (200): chain into loadUserStatus(). Login only authenticates — we still need to know who logged in. loadUserStatus()(dispatch) invokes the thunk manually (it returns (dispatch) => {...}, so we call it with dispatch). This is why the test expects 3 actions: BEGIN (login) + BEGIN (loadUserStatus) + SUCCESS (loadUserStatus).
(f) On failure: catchError(dispatch) dispatches USER_LOGIN_FAILURE with the error message — e.g. 'Fetch failed: Error 401' for wrong password.
APP STARTUP:
ProjectForge.jsx → dispatch(loadUserStatus())
└─ GET /rs/userStatus
├─ 200 → SUCCESS(userData, systemData, alertMessage)
└─ 401 → redirect to /react/public/login
LOGIN:
login(username, password, keepSignedIn)
├─ dispatch(BEGIN)
├─ POST /rsPublic/login
├─ server sets JSESSIONID (+ stayLoggedIn cookie if keepSignedIn)
├─ 200 → loadUserStatus()(dispatch)
│ └─ GET /rs/userStatus → BEGIN → SUCCESS
└─ 401/error → catchError → FAILURE
AFTER LOGIN:
User returns to original page. ProjectForge.jsx re-mounts.
loadUserStatus() runs again — this time JSESSIONID is valid → SUCCESS
Two utilities from rest.js:
getServiceURL(path)— builds the full URL: prependshttp://localhost:8080(dev) or""(prod).'/rsPublic/login'→ devhttp://localhost:8080/rsPublic/login.handleHTTPErrors(response)— throwsError('Fetch failed: Error {status}')if!response.ok.No other dependencies. The file is self-contained except for these two helpers.