React single-page application to display the frontend
This project was bootstrapped with Create React App.
- React - Define how frontend components render given the state.
- JSX - Define react components using tags similar to HTML or XML
- TypeScript - Variant of JavaScript that adds static typing
- Semantic UI React - CSS interface library
- React Router - Handles frontend routing for the single-page application
- Redux - Stores global application state
- SASS - Improved syntax for CSS
Other frameworks used for development:
- npm - Node package manager, downloads and installs JavaScript packages
- Babel - Transpiler so newer JavaScript syntax works in older browsers
- ESLint - Lints the TypeScript code to ensure good coding practices
- Prettier - Automatically formats the code
In the project directory, you can run:
npm start
This runs the app in the development mode on port 3030
.
Open http://localhost:3030 to view it in the browser.
The page will automatically reload if you make edits.
You will also see any lint errors in the console.
To compile the code for production, you can run:
npm run build
This command builds the app for production to the build
folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes, all ready to be deployed!
To run the frontend, you will need to set several environment variables.
The easiest way is to use a .env
file. You should either name the file:
.env.development
- Development environment variables.env.production
- Production environment variables
You will then need to set the following environment values:
Variable | Required | Default Value | Description |
---|---|---|---|
REACT_APP_API_BASE_URL | No | /api/v1 |
URL to access the proxy for the API server and collectors. See below for more details on configuring the proxy. |
REACT_APP_NOTIFICATIONS_BASE_URL | No | ws://localhost:3010/api/v1/notifications |
Websocket URL to subscribe to the notification server |
REACT_APP_RECAPTCHA_SITE_KEY | Yes | Site key used by Google reCAPTCHA. |
The development proxy is configured in package.json
. By default, it assumes it can access the API servers and collectors under localhost:3010
.
The API then uses /api/v1
as a replacement for localhost:3010/api/v1
. This can be changed by updating:
{
// ...
"proxy": "http://localhost:3010"
// ...
}
A basic development proxy can be configured using NGINX.
The code snippet below properly configures NGINX to access all services using localhost:3010
.
server {
listen 3010;
# Increase the reverse-proxy timeout
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
# Proxy the API server
location /api/v1/ {
proxy_pass http://localhost:3000/api/v1/;
}
# Proxy the mediator
location /api/v1/mediator/ {
proxy_pass http://localhost:3004/api/v1/mediator/;
}
# Proxy the notifications server as a reverse websocket proxy
location /api/v1/notifications {
proxy_pass http://localhost:3005/api/v1/notifications;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
# Proxy the collectors using /api/v1/collector/{ID}, where ID is the collector UUID
# This uses an internal route from the mediator to convert {ID} into a URL path
location ~ "^/api/v1/collector/([a-zA-Z0-9-_]{22})/(.*)$" {
set $auth_collector_id "$1";
auth_request /collector-path;
auth_request_set $collector_url $upstream_http_x_collector_url;
proxy_pass $collector_url/$2;
}
# Talk with the mediator to get the actual URI path to the collector
# The result gets stored in the "x-collector-url" header
location /collector-path {
internal;
proxy_pass http://localhost:3004/api/v1/mediator/collectors/$auth_collector_id/proxy;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_intercept_errors on;
# Trap all API errors other than 401 and 403
error_page 300 301 302 303 304 305 306 307 308
400 402 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451
500 501 502 503 504 505 506 507 508 510 511 @collector-proxy-error;
}
# Hide the internal proxy pass
location ~ "^/api/v1/mediator/collectors/([a-zA-Z0-9-_]{22})/proxy$" {
return 404 "";
}
# Handle any unexpected errors from the proxy
location @collector-proxy-error {
internal;
return 403 "";
}
}
Add this configuration as a file in /etc/nginx/sites-available
, then create a symbolic link in /etc/nginx/sites-enabled
to enable the configuration.
If NGINX is already running, use sudo nginx -s reload
to reload the configuration in real time.
On a production server, the REACT_APP_API_BASE_URL
will need to be updated to the public proxy URL running in the cloud.
Code Layout:
/src
- Main directory for all code files/public
- Other public files included in the website, like images, robots.txt, and the manifest
The main entry point for the entire application is src/index.tsx
, which stores the Redux global state and loads the router.
This component also defines the error boundary used for trapping JavaScript errors.
All of the routes for the application are defined in src/components/Routes.tsx
.
Subdirectories in src/
:
/api
- Types used to simplify API requests to the backend server/components
- React components used to render the pages/helpers
- Miscellaneous functions and structures/models
- Defines the public JSON return types from the API server/notifications
- Functions and hooks for communicating with the notification server/protocol
- Functions specific to the electronic voting protocol/redux
- Defines the global state for the application/semantic-ui
- Custom Semantic UI Theme for the website
Subdirectories of components/
:
/errorDialogs
- Shared component for showing application errors/input
- Various custom input components, like text boxes/routes
- The actual pages in the application/shared
- Other components shared by multiple pages in the application
The code repository uses TypeScript
, ESLint
, and Prettier
to lint the codebase and ensure a consistant format.
When working with this codebase, try to install plugins for these frameworks to automatically format your code when saving.
For example, VSCode provides good integration through the following plugins:
- TypeScript - Support built automatically into VSCode
- ESLint
- Prettier - Code Formatter
- SCSS IntelliSense
ConfirmDialog
- Used throughout the application to ask a yes-no questionErrorBoundary
- Catches all errors thrown by the applicationFlex
- Basically a<div>
element withdisplay: flex
set in the style. Allows easy formatting of complex flexbox layouts.TransitionList
- Apply a transition effect so children display one-by-one.
All pages in the program are defined in src/components/Routes.tsx
.
General guidelines:
- Routes are classified as either
logged out
,logged in
, orboth
- Each router entry is based on RouteProps from the React Router library. In general, routes define a path and a React component to render.
- Routes can also define a permission from the
Permission
enum needed to view a page. - Each route is defined as a sub-folder inside
src/components/routes/
- In general, the page is defined as
<Page>.tsx
with any actions as<page>Actions.ts
. (The component is capitalized, but the actions is lowercase). Routes may define nested components if needed. - Pages handle notifications events in
<page>Notifications.ts
. - Changing pages should be handled by React Router
history.push()
, NOT by updatingdocument.location
.
State in pages is handled using React hooks, except in rare cases where stateful class components are used. Most components store the global state using Redux, although trivial state values may be stored in local hooks.
For the most part, each route starts with the following three lines of code:
// Set the title for the page
useTitle('Page Title');
// Some page-specific hook to reset the Redux state
useResetPageState();
// One or multiple page hooks to load data from the API server
useFetchPageData();
The codebase provides a series of TypeScript interfaces and functions to assist with loading data from the backend API server.
API Data Types:
APIOption<T>
- Represents a type that is either loading or loaded. On failure to load, an error is thrown.APIResult<T>
- Represents a type that is loading, success, or an error.
API Functions:
apiLoading()
- Create a new loading objectapiSome(data: T)
- Create a new loadedAPIOption<T>
apiSuccess(data: T)
- Create a new successAPIResult<T>
apiError(error: Error)
- Create a new errorAPIResult<T>
API Constants:
axiosApi
- Special instance of the Axios Object that automatically refreshes expired JWT tokens.resolveOption
- UsePromise.then(...resolveOption)
to convert an Axios promise intoAPIOption<T>
resolveResult
- UsePromise.then(...resolveResult)
to convert an Axios promise intoAPIResult<T>
resolveOptionUnwrapped
- UsePromise.then(...resolveOptionUnwrapped)
to convert an Axios promise intoT
Often, the application only cares if an API result loaded without caring about error handling.
This is the purpose of APIOption<T>
, which either returns a loading or success response.
If an error occurs, it is thrown and caught by the ErrorBoundary
component.
The Redux JavaScript Library defines a system for storing the global application state.
The global state is stored in an object called the store
, with each page being a sub-property in the store object.
Any changes to the store must go through reducers
, which define how the store
changes for various actions.
Each reducer
takes an action
, which is just a plain JavaScript object, and changes the store
to build the new state.
Each action is constructed using an action creator
, which is a plain JavaScript function that returns an action
object.
State
- Stores global state of programReducer
- Makes changes to stateAction
- Defines action for the reducer to runAction Creator
- Builds the action objects
The global store itself is defined in /src/redux/store.ts
, and the data type is defined in /src/redux/state/root.ts
.
To isolate parts of the application, each page has a nested property inside the root state.
All of these nested states are defined in /src/redux/state
.
To simplify the use of Redux in the application, a universal action creator and reducer has been written to update any part of the state. Rather, the application defines several higher-order functions for interacting with the store:
nestedSelectorHook(state: keyof RootState)
- Returns a function that can be used likeuseSelector
, but with a nested state.getNestedState(state: keyof RootState)
- Returns a function that can be used to get the nested state.mergeNestedState(state: keyof RootState, object?: Partial<T>)
- If object is provided, merges the given properties in the nested state. If object is not provided, returns a higher-order function for merging the nested state.setNestedState(state: keyof RootState, object?: T)
- If object is provided, sets the nested state. If object is not provided, returns a higher-order function for setting the nested state.clearNestedState(state: keyof RootState, initial?: T)
- Clear the nested state inside the store. By default, it uses the original initial state.
In development mode, all of these functions are available from the global window object to help with debugging.
passwordComplexity
- Constants for the password complexity checkeruseTitle()
- Set the document title for the pageshowConfirm()
- Show the yes-no confirmation dialogisDev()
- Returnstrue
if the server is running in development mode