⚠️ Draft documentation
Work in progress — some sections may be incomplete, typos possible. Last updated: 2026-05-10.
EN | DE | RU | FR | ES
EN

Source walkthrough: src/actions/authentication.js

File: authentication.js on develop (85 lines)

Status: unchanged between develop → branch draft43npm

Tested by: actions/authentication.test.js

Related: reducers/authentication.js — processes the actions dispatched here

1. Imports (line 1)

import { getServiceURL, handleHTTPErrors } from '../utilities/rest';

Two utilities from rest.js:

No other dependencies. The file is self-contained except for these two helpers.

2. Action type constants (lines 3–5)

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).

3. Action creators (lines 7–26)

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.

CreatorSignatureWhat 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.

4. catchError helper (line 28)

const catchError = (dispatch) => (error) => dispatch(userLoginFailure(error.message));

Curried function: catchError(dispatch)(error).

Used in .catch() chains of both login() and loadUserStatus():

5. loadUserStatus — session check (lines 30–63)

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:

Dispatch: catchError(dispatch)({ message: undefined })error.message will be undefined. The reducer stores { error: undefined } — effectively clearing any previous error.

This is called:

6. login — authenticate (lines 65–85)

export 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:93LoginService.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.

Summary: action flow

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

Related docs: authentication.test.js diff commentary | reducers/authentication.js diff commentary