Code overview

October 25, 2025 · View on GitHub

The application is hosted on Heroku. The public URL (for this deployment) is www.prone-materiaalipankki.fi which is an alias directing traffic to Heroku.

Personal information is saved encrypted. This is likely over-provisioned for the current scope, but was implemented and tested in case customer (sensitive) information needs to be stored in future (for example healthcare-related customer records).

Role based access control

The application has a simple role based access control. Roles are visible in frontend/src/config/userRoles.js and may be extended there. The routes and page links are defined in frontend/src/config/RoutesConfig.jsx. For each route the allowed roles are defined in that config and enforced by the frontend and backend authorization middleware.

Note: The backend should always enforce role/permission checks — frontend checks are for UX only.

Access and refresh tokens

The application uses short-lived access tokens and longer-lived refresh tokens. Access tokens are stored in localStorage; refresh tokens are stored as cookies and handled by the backend login controller.

The backend login controller exposes a refresh endpoint under the login API, for example: POST /api/login/refresh. The frontend uses Axios interceptors (frontend/src/services/api.js) to detect 401 Unauthorized responses caused by an expired or invalid access token. On receiving a 401 the frontend will attempt to refresh the access token using the refresh token (sent as an HTTP cookie). If refresh succeeds, the frontend retries the original request with the new access token. If refresh fails (refresh token expired/invalid) the frontend clears local state and redirects the user to the login page.

Important nuance:

  • A 401 from the backend typically means the access token is invalid or expired.
  • The frontend then calls the refresh endpoint with the refresh token.
  • If refresh returns 200 with a new access token, the original request is retried.
  • If refresh fails (e.g., refresh token has expired), the user is logged out.

The frontend implements a queue to avoid concurrent refresh requests when multiple requests fail at once. See frontend/src/services/api.js for the implementation details.

Sequence diagram (high level)

    sequenceDiagram
    participant Browser
    participant Frontend
    participant Backend

    Browser->>Frontend: Make API request
    Frontend->>Backend: GET /resource with accessToken (Header)
    Backend-->>Frontend: 401 Unauthorized (accessToken expired)

    alt isRefreshing == false
        Frontend->>Backend: POST /api/login/refresh (refreshToken via Cookie)
        Backend->>Backend: Validate refreshToken (JWT verify)
        alt Refresh token valid
            Backend-->>Frontend: new accessToken
            Frontend->>localStorage: Save new accessToken
            Frontend->>Backend: Retry original request
            Backend-->>Frontend: 200 OK
                else Refresh token expired/invalid
                    Backend-->>Frontend: 401 Unauthorized
                    Frontend->>Browser: Clear localStorage, redirect to /
                end
            else isRefreshing == true
                Frontend->>Queue: Add to failedQueue
                Queue-->>Frontend: Wait for refresh to complete
                Frontend->>Backend: Retry original request with new accessToken
                Backend-->>Frontend: 200 OK
            end

Cloud service / timeouts

The application runs in a cloud service; the current deployment uses a singledyno/VM which may hibernate and cause an initial cold-start delay. Because of this, reasonable timeouts are configured in the frontend API client.

frontend/src/services/api.js defines a global request timeout and handles token refresh. The login service (frontend/src/services/login.js) sets its own axios timeout and uses withCredentials: true for cookie-based refresh/logout.

Some browser extensions may interfere with login in certain environments (there are known cases where an extension causes an error like Cannot read properties of undefined (reading 'data') during login). Opening the site in an incognito mode sometimes avoids such extension-caused issues.

Materials and deleted users (defensive behavior)

The backend materials endpoints return material metadata and the creating user (if present). In deployments where user rows might be removed (full delete), the materials endpoint has a defensive fallback: when no creating user is present the API returns a harmless placeholder user object (for example: first_name: 'poistettu', last_name: 'käyttäjä') so the frontend can render material details without runtime errors.

If you plan to implement irreversible anonymization instead of deletion, a recommended improvement is to populate a non-PII display name on the material at creation time (for example authorDisplayName on the materials table). That way you can preserve readable author attribution without keeping the user's PII in the system.

Storage of PII and backups

Personal data is encrypted (see backend/utils/encryptions.js). Be aware that encrypted data is still personal data if the encryption key exists and can be used to decrypt. For GDPR erasure requests you must ensure any backups or logs that contain PII are handled according to your retention policy and legal requirements.

Testing

Unit tests are written with Vitest. Integration/E2E tests are implemented with Playwright. The e2e test suite uses a separate PostgreSQL database running in Docker for test isolation — make sure that DB is running and the test back- and frontend are started in test mode before running E2E tests to avoid affecting production data.

Quick commands (examples)

# Run unit tests (from repo root or frontend/backend as needed)
# If your package.json has a test script, run:
npm test

# Or run vitest directly from the frontend
cd frontend
npx vitest

# Run Playwright e2e tests (start back- and frontend in test mode first, ensure the Postgres test DB Docker container is running)
npx playwright test

Other notes

  • Role names are defined in frontend/src/config/userRoles.js.
  • Routes with required roles are configured in frontend/src/config/RoutesConfig.jsx.
  • The backend uses auth middleware to enforce required roles for each API route.
  • Avoid storing PII in application logs; mask or redact sensitive fields in structured logs if possible.
  • For lawful retention requirements or legal holds you should document the justification and retention periods.

Cloud / operational tips

  • Keep a documented process to handle PII in backups (deletion/scrubbing or retention justification).
  • Revoke refresh tokens and active sessions when anonymizing or deleting users.
  • Use transactions for anonymization/delete operations that must update multiple tables to avoid partial state.