# Technical documentation — ipw-frontend

Evidence date: 2026-10-11.

Source: `git@bitbucket.org:overdrive/ipw-frontend.git`, `main` at `798bd76` (2025-06-03).

## What this repository is

A Nuxt 3 client, generated as a static site (`ssr: false`, `target: 'static'` in `nuxt.config.js`). Package name `ipw-client`, version `0.0.2`, `private: true`, license `UNLICENSED`. Node engine `^18.0.0`.

There is no server, database schema, queue, or Dockerfile in this tree. Persistence and authorisation beyond the route guards live in whatever process serves `NUXT_API_LOCATION`.

## Application structure

```text
app.vue                 router title switch (en title / afr titel)
error.vue               status, message, and stack
layouts/default.vue     loading, menu, navbar, banner, sidebar, toast
pages/                  file-based routes
components/             sidebar, navs, member and role forms, adobe prompt
composables/            jwt, cookies, roles, auth class, validation, localStorage
api/                    fetch clients
middleware/             is-logged-in, is-not-logged-in, is-administrator
plugins/session.js      useState defaults
i18n.config.ts          global en/afr strings
public/                 favicons, images, PDFs
scss/                   styles.scss (global CSS entry)
assets/                 adobe reader images
forms/, notifications/, modules/   older kit, not referenced by pages
```

`imports.autoImport` is false. Pages import composables and components by path. `tsconfig.json` extends `./.nuxt/tsconfig.json`. Application code is JavaScript except `i18n.config.ts`.

`plugins/session.js` initialises `loggedIn`, `pending`, `showMenu`, `showNotify`, `showRelogin`, and `selectedProfile`.

## Runtime configuration

`nuxt.config.js` `runtimeConfig.public`:

| Key | Source | Used for |
|---|---|---|
| `ENV` | `process.env.ENV` | Credential mode: `staging` and `production` use `same-origin`; anything else uses `include` |
| `NUXT_API_LOCATION` | `process.env.NUXT_API_LOCATION` | Origin prefixed to every `/api/...` call |

`.env.template` (not loaded by a file reference in `nuxt.config.js`; Nuxt's own env loading applies):

```text
ENV=development
HOST=127.0.0.1
PORT=8080
NUXT_API_LOCATION=http://127.0.0.1:9000
```

The comment in that file says Nuxt forces `NODE_ENV=production` for generated builds, so `ENV` is a separate switch. `HOST` and `PORT` are not read in `nuxt.config.js`. Whether the Nuxt CLI honours them is **Unknown / Requires Confirmation**.

`.gitignore` ignores `.env`, `.env.staging`, `.env.production`, `node_modules`, `.nuxt`, `.output`, and `dist`.

## Scripts

| Script | Command |
|---|---|
| `start` | `nuxi dev` |
| `build:dev` | `ENV=development nuxi generate` |
| `build:staging` | `ENV=staging NUXT_API_LOCATION=https://staging.ipw.co.za nuxi generate` |
| `build:production` | `ENV=production NUXT_API_LOCATION=https://ipw.co.za nuxi generate` |
| `preview` | `nuxi preview` |
| `postinstall` | `nuxi prepare` |

The `ENV=` prefix is a Unix shell assignment. On Windows cmd it is not valid syntax. This repository has no `cross-env` dependency. **Inferred:** the scripts are written for a Unix shell (the Bitbucket pipeline image, or a developer shell that accepts that syntax).

`README.md` documents `npm run dev`, `generate-dev`, `generate`, and `generate-production`, and an output directory `client/.output/public`. Those script names and that directory are not in this tree.

## Dependencies

Runtime: `@pinia/nuxt` `^0.5.1` (module commented out in config), `bootstrap` `^5.3.6`, `cookie` `^0.5.0`, `email-regex` `^5.0.0`, `google-libphonenumber` `^3.2.33`, `jquery` `^3.7.1`, `js-cookie` `^3.0.5`, `jwt-decode` `^3.1.2`, `owasp-password-strength-test` `^1.3.0`, `unhead` `^1.7.0`, `vue-multiselect` `^3.0.0-beta.2`.

No `import` of `jquery` was found under the Vue and JS sources. It may be unused. That was not proven by a bundle build.

Development: `@nuxtjs/i18n` `^8.0.0-rc.4`, `nuxt` `^3.17.4`, `sass` `^1.89.0`.

`.npmrc`: `shamefully-hoist=true`, `strict-peer-dependencies=false`.

`package.json` `repository`, `bugs`, and `homepage` point at `https://github.com/WeboonTechnologySolution/ipw.co.za.com`. The clone remote is `https://bitbucket.org/overdrive/ipw-frontend.git`.

## API client

`api/index.js` builds `{NUXT_API_LOCATION}/api/{model}`.

| File | Model | Methods that are live |
|---|---|---|
| `api/auth.js` | `auth` | `POST /login`, `/reset`, `/refresh`, `/logout` |
| `api/user.js` | `user` | contact-details, groups-and-roles, available-groups, join-group, pending-groups, cancel-pending-group, `PUT /member`, `PATCH /:id`, `GET /:id`, available-profiles |
| `api/admin.js` | `admin` | all-users, get-user, create-user, update-user, member-activity, live-users, role add/remove, all-members, groups-and-roles, available-groups, add-member-role, remove-group-membership, add-group-membership, all-groups, delete group |
| `api/upload.js` | `upload` | categories, documents, document (multipart, no JSON content-type), all-profiles, all-documents, `GET download-document/:id` |
| `api/report.js` | `report` | years, group-names, control-products, table-report, csv-report (blob) |
| `api/role.js` | `role` | descriptions, all-descriptions, `GET user/:id` |

`GET {base}/api/csrf` returns the token as text. The status code is not checked.

Authenticated JSON calls set `Content-Type: application/json`, `x-csrf-token`, and `Authorization: Bearer {header.payload}`. Upload calls omit `Content-Type` so the browser can set the multipart boundary.

Most methods catch errors and return `{ err }` instead of throwing. Callers then test `err.message === 'LOGIN_EXPIRED'`.

`pages/admin/member-registrations.vue` calls `admin.newRegistrations()`. That function is not on the class. Opening the page and letting it mount will throw.

## Authentication mechanics

Cookies are read by `js-cookie` as `__${document.location.hostname}-access` and `__${document.location.hostname}-refresh` (`composables/useCookies.js`). If `document` is undefined the read returns null.

`getRaw` keeps only the first two JWT segments and drops the signature (and anything after it) before `jwt-decode` and before the `Authorization` header.

`isAccessValid` refreshes when the access token is expired and the refresh token is not. Refresh success is HTTP status 200. The new cookie, if any, must be set by the response. The client does not write it. `cookieOptions` (httpOnly, secure, signed, one-minute `maxAge`) is never passed to `Cookies.set`. Nothing in the app calls `setItem`.

`useAuth` reads `user_class_id` from the refresh token. `useRoles` reads `roles` and `sub` from the access token (`getDecoded()` defaults to `access`).

Middleware files are `isLoggedIn.js`, `isNotLoggedIn.js`, and `isAdministrator.js`. Nuxt kebab-cases those names to `is-logged-in`, `is-not-logged-in`, and `is-administrator`.

## Internationalisation

`@nuxtjs/i18n` with `vueI18n: './i18n.config.ts'`. `legacy: false`, `locale: 'en'`, `globalInjection: true`. Messages cover shared errors, table empty states, and a few field labels. Page copy is mostly in `<i18n>` blocks on each Vue file, locales `en` and `afr`.

`app.vue` sets `document.title` from route meta on each navigation.

## UI stack

Bootstrap 5 and global SCSS (`scss/styles.scss`). `vue-multiselect` on group join and the staff document profile picker. `layouts/default.vue` shows `Loading` while `pending` is true. `Toast` is the shared error and success surface (`useState('toast')`).

Page transition is a 0.2s opacity fade (`app.vue`).

`experimental.payloadExtraction` is false. A comment asks why it had been enabled.

## Routes that matter for operators

Public (no auth middleware): `/`, `/cellars`, `/about-us`, `/manuals`, `/forms`, `/literature-and-articles`, `/audit`, `/useful-contacts`, `/courses`, `/climate-change`, `/greenhouse-gas-protocol`, `/environment-sustainability-initiatives`, `/links`, `/contact-us`, `/forbidden`, `/reset-instructions`, `/registration` (guard commented out), `/confirm/:type/:hash/:token`, `/reminder`.

Login wall: `/login` (`is-not-logged-in`).

Signed-in: `/member/contact-details`, `/member/groups-and-roles`, `/member/guidelines`, `/member/codings`, `/member/reports`, `/member/edit` (duplicate of reports, unlinked), `/upload/documents`, `/management/list-members` (duplicate of documents, unlinked).

Signed-in and administrator name: everything under `pages/admin/`.

## Security notes visible in the client

- Login is username and password over `fetch` with CSRF. No captcha and no second factor in the Vue page.
- The bearer token is the unsigned header and payload. The signature segment is stripped in the browser before the request.
- `error.vue` renders `error.stack` in the page.
- `isAdministrator` prints the role list with `console.log`.
- Cookies are readable from JavaScript (`js-cookie`). The unused `cookieOptions.httpOnly` flag would not apply to cookies the API sets, and `js-cookie` cannot read an httpOnly cookie.
- `middleware/isLoggedIn.js` uses `redirectCode: 301` for the login redirect. On a client-only navigation that status is a Nuxt option, not an HTTP response this static file itself emits.

## Pipeline, quality, and tests

`bitbucket-pipelines.yml` runs on branch `main` only. One step, `sonarsource/sonarqube-scan:3.0.2`, with `SONAR_HOST_URL` and `SONAR_TOKEN` from repository or workspace variables, `EXTRA_ARGS=-Dsonar.scanner.forceEmbeddedJava=true`, and Docker memory 3072 MB. Clone depth is full. There is no build, test, or deploy step.

`sonar-project.properties` sets only:

```text
sonar.projectKey=overdrive_ipw-frontend_b7c05f21-9a53-4873-bdbf-f6fafac5a199
```

No test script, no `tests/` directory, and no Jest or Vitest config. This inspection did not install dependencies or run the app.

## Related repositories

Documented in Silicon Ark, not imported by this code:

- `ipw_new` — NestJS API and a Nuxt client with the same package name and version. Tip `111772e` (2025-05-26), two days before this repo's application import.
- `ipw-php` — CodeIgniter 4 IPW application, commits through 2026-09-28.

This frontend does not name either repository. Which one, if either, is the API behind `https://ipw.co.za` is **Unknown / Requires Confirmation**.

## Known defects in this revision

These are observations about the code as written, not a product decision to change it.

1. Reminder success path uses `username.value`, which is not defined (`pages/reminder.vue`).
2. Registration submit calls `auth.login` and then `form.password` / `email.value` (`pages/registration.vue`).
3. Confirm submit handlers are empty.
4. `admin.newRegistrations` is missing.
5. Create-user last-name validation reads `form.firstname`. `resetForm` assigns other fields onto `form.title`.
6. Afrikaans biodiversity guideline links to the English PDF.
7. `pages/member/edit.vue` and `pages/management/list-members.vue` duplicate live pages and are not in the menu.
8. Evaluation-form, member-form, probation, certificate, SAWIS, and add-group screens are static samples, including forms whose submit function is not defined.
9. README scripts, paths, and several route names do not match the tree.
10. `error.vue` calls `nextTick` without importing it (`router.afterEach`).
