# PRD — unity-skeleton (Unity / UnityReact)

Evidence date: 2026-09-30. Source: `https://bitbucket.org/overdrive/unity-skeleton.git`, `master` `7b94f92` (2020-10-29).

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No formal PRD existed in the repository or in `silicon_ark/unity-skeleton/` before this file. Requirements below are recovered from routes, sagas, the mock API, and seeds. They are not a new product decision.

Identifier values in `src/config.js` are omitted. The maps key in `.env` is omitted. The register form's default email and password literals are omitted; the file that holds them is named.

## Product purpose

A person opens a web app titled **UnityReact**. The shell brands itself **Unity**. They can open login, register, and the one-time-code screen without a session. Any other route requires a role of `superuser` or `user`. A completed Cognito sign-in always stores the role `superuser`, then sends them into the shell. The home path `/` redirects to `/profile`.

**Implemented** in `public/index.html`, `src/app/layouts/shared-components/Logo.js`, `src/app/configs/routesConfig.js`, and `src/app/store/sagas/auth/LoginSaga.js`.

The README does not state a product purpose. It is the Create React App boilerplate. Provenance of the README: **Documented**. Provenance of the product behaviour: **Implemented**.

Three names are in the tree and they are not the same string: page title `UnityReact`, brand `Unity`, package name `fuse-react-app`. Which name is the product name is **Unknown / Requires Confirmation**.

## Users and roles

| Role | How it is assigned | What the code distinguishes | Provenance |
|---|---|---|---|
| Guest | Initial Redux user `role: []` (`src/app/store/reducers/auth/user.reducer.js`) | May open routes whose `auth` array is empty: `/login`, `/register`, `/registerTest`, `/otp` | **Implemented** |
| `superuser` | Hard-coded in `LoginSaga` and `InitAuthSaga` after a Cognito session and a uuid lookup. Also the mock seed user in `src/@fake-db/seeds/Auth.js` | `FuseUtils.hasPrivilege` returns true for this role without reading a privilege list. Route auth includes this string | **Implemented** |
| `user` | Mock register handler `src/@fake-db/api/Auth.js` sets `role: "user"` and copies group privileges. The screen's register saga does not call that handler | Included in the default route auth array. No Cognito path assigns it | **Implemented** in the mock. Not reached by `RegisterSaga` |

`src/app/auth/authRoles.js` lists only `{ id: "superuser", privileges: "*" }`. Commented-out mock users for `admin`, `staff`, and `user` remain in the auth seed and are not loaded.

`UnityAuthorization` (`src/@unity/UnityAuthorization/UnityAuthorization.js`) allows a route when `FuseUtils.hasPermission(route.auth, user.role)` is true. It does not consult privileges.

- `auth` null or undefined → allow.
- `auth` empty array → allow only when `user.role` is missing or empty (guest).
- Otherwise the user's role string or role array must intersect `auth`.

`routesConfig.js` passes `["superuser", "user"]` as the default auth for every generated route. Login, register, and OTP configs set `auth: []`, which replaces that default for those configs (`FuseUtils.setRoutes` uses `config.auth` when present).

Signed-in state for the route gate is the Redux `auth.user.role`, not a live Cognito check on each navigation. Session restore reads `localStorage.jwt_access_token`, checks `exp` with `jwt-decode`, then calls Cognito `Auth.currentUserInfo` and the uuid API. Provenance: **Implemented** in `InitAuthSaga.js`.

What the `unity` API returns beyond `strUser`, `strEmail`, and `strCognitoUuid` on the first array element is **Unknown / Requires Confirmation**. The client reads only those three fields and then discards any role the API might have sent.

## Major workflows

### 1. Open the app

Trigger: browser loads the CRA bundle (`src/index.js`). `serviceWorker.js` exists and is not imported.

Behaviour:

- Amplify is configured for Auth (Cognito, `mandatorySignIn: true`), Storage (S3 bucket `unity-app-files`), and an API endpoint named `unity`. Region in config is `eu-west-1`. Ids live in `src/config.js`.
- `App` imports `@fake-db`, which installs an axios mock adapter and seeds `localStorage` tables only when each key is absent (`src/@fake-db/seed.js`).
- Redux store uses redux-persist (`key: "root"`) with `fuse`, `auth`, and `SalesModule` blacklisted. Auth therefore survives in `jwt_access_token`, not in the persisted reducer.
- `Auth` dispatches `INIT_AUTH_REQUEST` and shows `FuseSplashScreen` while `auth.AuthCheck` is true.
- `UnityAuthorization` wraps the router. Guests hitting a protected route are sent to `/login` with `state.redirectUrl`. A signed-in person hitting a guest-only route is sent to `redirectUrl` or `/`.
- `/` redirects to `/profile`. Unmatched paths redirect to `/error`.

Output: splash, then either the shell or a redirect to login.

### 2. Sign in

Trigger: Login button on `/login`, `JWTLoginTab` → `submitLogin` → saga `LOGIN_REQUEST`.

Inputs: `email`, `password`.

Validation (**Implemented**, `JWTLoginTab.js`):

- Both fields required.
- Each must be at least 4 characters ("Min character length is 4").
- No email-format check.

Behaviour when validation passes:

- Message "Attempting login".
- `Auth.signIn(email, password)`.
- On a session, the Cognito access token is stored as `localStorage.jwt_access_token` and set as `Authorization: Bearer` on axios. The token value is also written to the console (`console.log` in `setSession`).
- `Auth.currentUserInfo().username` is sent to `API.get("unity", "/unity/uuid/{username}")`.
- If `JSON.parse(response.body)[0]` exists, display name, email, and uuid are taken from `strUser`, `strEmail`, `strCognitoUuid`. Photo is the static asset `assets/images/avatars/Abbott.jpg`. Role is the string `superuser`.
- If that element is missing, the same shape is returned with display name `newUser`, email the literal `strEmail`, uuid the literal `strCognitoUuid`, and role `superuser`.
- `LOGIN_SUCCESS` is dispatched. Where the app navigates after that is determined by `UnityAuthorization` (guest routes bounce a signed-in role toward `/` or `redirectUrl`), not by an explicit `history.push` in the login saga.

Errors:

- Cognito `UserNotConfirmedException` → navigate to `/otp` with `state.email`, message "Please supply your confirmation pin".
- Any other thrown error → message `e.message` and `LOGIN_ERROR`.

The second button on the same form is labelled **Reset Password**, has `type="submit"`, and has no separate handler. **Implemented**: it submits the login form. No `Auth.forgotPassword` call exists under `src`.

### 3. Register

Trigger: `/register` (`Register2Page` / `JWTRegisterTab`) → `REGISTER_REQUEST`.

Inputs on the form: `email`, `password`, `validatePassword`. Each required, min length 4. The three fields ship with a non-empty default literal in `JWTRegisterTab.js` (value omitted here). There is no validation that `password` equals `validatePassword`. The submit button label is **Login**.

`RegisterSaga.asyncRequest` calls `Auth.signUp({ username: email, password, attributes: {} })`. The saga function accepts `displayName` and `group` and does not send them to Cognito. On success it navigates to `/otp` with `state.email`.

The signup `catch` logs the error and does not rethrow. The generator then always dispatches the message "We sent a code to your email!" and `REGISTER_SUCCESS`. **Implemented**. A failed sign-up still shows the success message and does not navigate.

`/registerTest` loads `RegisterReWrite.js`, which is a second register layout ("Welcome to Unity!") and is not linked from the login screen's Register link.

The mock `POST /api/auth/register` creates a `localStorage` user with role `user`. The register saga does not call it.

### 4. Confirm the code

Trigger: `/otp`, `CONFIRM_OTP_REQUEST` with `otp` and `location.state.email`.

Behaviour: `Auth.confirmSignUp(email, code)`. If the return value is the string `SUCCESS`, message "Account confirmed please sign in" and navigate to `/login`. Otherwise message "Wrong/Expired OTP please resend or contact your admin".

`confirmSignUp` catches errors, logs them, and returns undefined, which takes the failure branch.

Resend (`RESEND_OTP_REQUEST`) calls `Auth.resendSignUp(email)` without `yield`. Failures are logged inside the async function. Provenance: **Implemented** in `ResendOTPSaga.js`.

If `/otp` is opened with no `location.state`, reading `location.state.email` throws. **Inferred** from `OTPFORM.js`. Not executed for this document.

### 5. Restore a session

Trigger: every load, `INIT_AUTH_REQUEST`.

If `jwt_access_token` is present and `exp` is in the future, the bearer header is set, the Cognito username is read, and `GET /unity/uuid/{username}` runs. Unlike login, a missing first element throws during destructure and the catch returns the error object as the "user". `setUserData` is still dispatched with that value. **Implemented** difference between `LoginSaga.asyncUserObjRequest` and `InitAuthSaga.asyncUserObjRequest`.

If the token is expired, `setSession(null)` runs and `userActions.logoutUser()` is called without `dispatch`. **Inferred**: the logout action creator's result is discarded, so Redux may keep the previous user until something else clears it.

An axios response interceptor treats HTTP 401 as "Invalid access_token" and calls `logoutUser()` the same way, without `dispatch`.

### 6. Use the shell

Trigger: `UnityLayout` mounts and dispatches `INIT_MODULE_ACCESS`.

Static nav (`src/app/configs/navigationConfig.js`):

- Profile → `/profile`
- Theme Settings → `/themesettings` (privilege `viewThemeSettingsScreen`)
- Manage (any of `viewUsersScreen`, `viewModulesScreen`, `viewRolesScreen`, `viewPrivilegeScreen`)
  - Users → `/users` (`viewUsersScreen`)
  - Modules → `/modules` (`viewModulesScreen`)
  - Groups → `/groups` (`viewGroupsScreen`)
  - Privileges → `/privileges` (`viewPrivilegeScreen`)

There is no route whose path is a roles screen. `viewRolesScreen` appears only on the Manage parent. **Implemented**.

`POST /api/getAllowedModules` returns every row in `localStorage.moduleDB` and ignores the posted `uuid` and `role`. The seed rows are Sales (`/sales`, `viewSALES`), Ticketing (`/ticketing`, `viewTicketingModule`), and Example Module (`/ExampleModule`, `viewExampleModule`). Those items are concatenated onto the nav.

Nav items are hidden only when **both** `hasPermission(item.auth)` and `hasPrivilege(item.privilege)` fail (`NavVerticalItem.js` and the sibling nav components). Items in this tree have a `privilege` and no `auth`. `hasPermission(null)` returns true, so the privilege check does not hide them. **Implemented**. A guest who could see the nav would see every item; the route gate still redirects them to `/login` before the shell is useful. A `user` role (only creatable via the unused mock register) could open `/sales` by URL because route auth allows `user` and route privilege is not enforced.

`FuseUtils.setRoutes` copies `config.privilege`, not `route.privilege`. The privileges route sets `privilege: 'viewProfile'` on the route object; that value is overwritten with null. `viewProfile` is not in the privilege seed. **Implemented**.

### 7. Profile and theme

`/profile` renders a header **John Doe** and avatar `Velazquez.jpg`, plus `AboutTab`. It does not read `auth.user`. **Implemented** in `ProfilePage.js`.

`/themesettings` header text is **User Management**. The body is an `h1` "Theme settings" and Fuse's settings panel (`FuseSettings`). **Implemented**.

### 8. Sales

Routes (`SalesConfig.js`), footer hidden:

| Path | Screen |
|---|---|
| `/sales` | Client grid, column picker, Excel upload, links to related screens |
| `/sales/new` | New client |
| `/sales/client` | View client (management, sales tracking, credit application) |
| `/sales/aws` | AWS sales team |
| `/sales/credit` | Credit types |
| `/sales/type` | Sales types |

Data path: sagas `POST` to `/api/getSalesClients`, `/api/addSalesClients`, `/api/updateSalesClients`, and the matching type, credit, and team endpoints. Handlers in `src/@fake-db/api/SalesClients.js` read and write `localStorage`. Nothing in the sales sagas calls Amplify.

The new-client schema (`NewUserSchema.js`) is an AWS commercial record: client, company number, CloudHealth link, account, root address, contact, country, opportunity from/date, AWS sales rep, AWS account manager, APN instruction / listed / stage / update date, Silicon billing status, engagement and migration dates, credits (activate, POA, WAR), CSAT, case study, margin, and a nested `creditTypes` list. Several boolean flags default to `false` (`cloudHealthReports`, `creditGranted` with comment "not mapped yet", and others). `companyNumber` is declared twice in the object literal; the second wins. **Implemented**.

Whether this schema matches a real Silicon sales process is **Unknown / Requires Confirmation**. The field names are evidence of intent, not a signed business rule.

### 9. Ticketing

Routes: `/ticketing`, `/ticketing/view`. A `/ticketing/dashboard` route is commented out.

`/ticketing` has five tabs: `list`, `assigned`, `completed`, `Tab 4`, `Tab 5`. Only `list` renders `TicketList`. The other four render empty containers; their components are commented out. A comment says `TODO - Change this to TableList`. **Implemented** / placeholder.

Add-ticket fields (`AddTicketModal.js`): Name, Description, Board, Status, Priority, Department, Client, AssignedTo. Boards are hard-coded: Maintenance Requests, Project Tickets, Service Requests. The comment in that file is "Mock Data Dump".

Ticket, task, and document sagas post to `/api/addTicket`, `/api/updateTicket`, `/api/deleteTicket`, `/api/getTickets`, `/api/getSelectedTicket`, task add/update/delete, and `/api/addTicketDocument` / `/api/getTicketDocument`. All are mock handlers. `src/config.js` defines `MAX_ATTACHMENT_SIZE: 5000000` and no saga reads it.

### 10. Users, groups, privileges, modules

| Screen | Path | Data |
|---|---|---|
| Users | `/users` | Table from `POST /api/getUsers`. Selecting a row shows `ViewUser` on the same page (privilege checkboxes → `POST /api/updateUserPrivileges`) |
| Groups | `/groups` | `POST /api/getGroups`, add, delete, add privileges. Selecting a row shows `ViewGroup` on the same page |
| Privileges | `/privileges` | List, add, delete via `/api/getPrivileges`, `/api/addPrivilege`, `/api/deletePrivilege` |
| Modules | `/modules` | `POST /api/addModule` appends a nav item and a privilege row in `localStorage` |

Seed group (`GroupDB`): one row, type `superuser`, privileges `"*"`.

Seed privileges (`PrivilegesDB`): `viewThemeSettingsScreen`, `viewUsersScreen`, `viewModulesScreen`, `viewGroupsScreen`, `viewPrivilegeScreen` (type `screen`, `systemPrivilege: true`); `viewSALES` (type `module`, `systemPrivilege: false`); `viewTicketingModule` and `viewExampleModule` (type `module`, `systemPrivilege: true`). The seed comment says the list is empty by default because superuser has all privileges, then defines these rows anyway.

`UserTable` tries to hide users unless the viewer is `superuser` or holds a privilege named `{displayName}View`. The mock register path creates that privilege. Cognito registration does not.

## Integrations

| System | Purpose | Direction | Auth | Failure |
|---|---|---|---|---|
| Amazon Cognito | Sign-in, sign-up, confirm, resend, current user | App → Cognito via Amplify | User pool configured in `src/config.js` | Login shows `e.message`, except unconfirmed users go to `/otp`. Sign-up errors are swallowed (see workflow 3) |
| API named `unity` | Load display name and email by Cognito username | App → API Gateway `GET /unity/uuid/{username}` | Amplify API (IAM/Cognito signing is whatever Amplify applies; custom headers are not set in `index.js`) | Login: empty body becomes a fake `newUser` still marked `superuser`. Init: exception object stored as user. Other HTTP failures were not given a dedicated message in these two functions |
| S3 `unity-app-files` | Configured as Amplify Storage | No call site found | Identity pool id in config | Not exercised by this tree |
| Browser `localStorage` | Users, groups, privileges, modules, sales, tickets, and the mock auth table | Read/write in-page | None | `JSON.parse` of a missing table returns null and callers can throw. Not separately handled in every saga |
| Google Maps key | Present in `.env` | No call site found | Key in repo | Unused by the inspected source |

The Lambda or other service behind `/unity/uuid/` is not in this repository.

## Constraints

- Node toolchain expected by the scripts: Yarn (`yarn start`, `yarn run build`). `package-lock.json` and `yarn.lock` are both committed.
- Dev server URL **Documented** in the README: `http://localhost:3000`.
- Production build runs Tailwind, then PurgeCSS, then `react-scripts build` with `GENERATE_SOURCEMAP=false`.
- Browserslist production: `>0.2%`, not dead, not Opera Mini. Development includes IE 11.
- `MAX_ATTACHMENT_SIZE` is 5_000_000 bytes and is unused.
- ThemeForest regular license is the only license text (`LICENSE` is a URL). What that allows for this private Bitbucket repo is **Unknown / Requires Confirmation**.

## Requirements discovered from the implementation

### FR-001 — Cognito sign-in

The system signs a person in with email and password through Cognito, stores the access token, and loads `/unity/uuid/{username}`.

Evidence: `JWTLoginTab.js`, `LoginSaga.js`, `src/index.js`, `src/config.js`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Unconfirmed user

If Cognito raises `UserNotConfirmedException`, the system opens `/otp` with the email in location state.

Evidence: `LoginSaga.js`, `OTPPageConfig.js`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Role after sign-in

A successful uuid lookup, and a lookup that returns no row, both store `role: "superuser"`.

Evidence: `LoginSaga.js` both return branches.

Implementation: Implemented. Whether that is the intended rule is **Requires Confirmation**.

Confidence: High that the code does this. Low that it is an approved rule.

### FR-004 — Guest route gate

Routes other than login, register, registerTest, and otp require role `superuser` or `user`. Guests are redirected to `/login`.

Evidence: `routesConfig.js`, auth page configs, `UnityAuthorization.js`, `FuseUtils.hasPermission`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Sales client record

A signed-in person can list, create, and update sales clients, credit types, sales types, and AWS team members. Persistence is the browser mock, not the `unity` API.

Evidence: `SalesConfig.js`, `src/app/store/sagas/modules/Sales/*`, `src/@fake-db/api/SalesClients.js`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Tickets

A signed-in person can list, add, update, and delete tickets, tasks, and ticket documents through the mock API. Only the list tab has a UI. Assigned, completed, and the two unnamed tabs are empty.

Evidence: `TicketingConfig.js`, `Ticketing.js`, ticket sagas, `src/@fake-db/api/Tickets.js`.

Implementation: Implemented for the list and the mock writes. Other tabs are placeholders.

Confidence: High.

### FR-007 — Privilege nav

Privilege strings are stored and edited, and nav items declare them. With the current boolean, missing `auth` on a nav item shows the item even when the privilege is absent.

Evidence: `navigationConfig.js`, `FuseUtils.hasPrivilege`, `NavVerticalItem.js`, privilege sagas.

Implementation: Implemented. Whether the OR is a defect is **Requires Confirmation**.

Confidence: High on the code path. The product intent is unknown.

## Out of scope for this document

Game engine "Unity" projects. The separate repository `unity-soft-ui`. Any behaviour of the `unity` API that this client does not call.
