# Features — ipw-backend

Evidence date: 2026-10-11. Revision: `main` `f5ff052` (2025-06-03).

There is no feature list in the source repository beyond the README. Status below is from `src/`, `package.json`, `deploy/`, `bitbucket-pipelines.yml`, and git history. Secret values are omitted.

## Current features

| Feature | Status | Where |
|---|---|---|
| NestJS HTTP server, global prefix `api` | Current | `src/main.ts` |
| Environment file `.env.${NODE_ENV}` | Current | `src/app.module.ts` |
| CORS from `CLIENT_ORIGIN` JSON | Current | `src/main.ts` |
| Static files: `client/.output/static` in development, `dist/static` otherwise | Current. The client build is not in this repo | `src/app.module.ts` |
| Login with username or email | Current since `f01f621` | `AuthService.checkNewPasswordStorage`, `checkOldPasswordStorage` |
| Reject `status <> 1` or `suspended <> 0` at login | Current | same methods |
| Argon2 password check against `user_password` | Current | `AuthService.checkNewPasswordStorage` |
| MD5 check against `users.password`, then copy into `user_password` when that row is missing | Current | `AuthService.checkOldPasswordStorage` |
| Update `last_logon_date` after a successful check | Current since `f01f621` | `AuthService.validateUser` |
| Access JWT 15 minutes, refresh JWT 1 day | Current | `AuthService.getTokens` |
| Split JWT cookies plus `Authorization` bearer | Current | `AuthService.setClientCookies`, `parseReqToken` |
| Store refresh token as argon2; clear it on logout | Current | `updateRefreshToken`, `logout` |
| Login body includes `redirect: '/member/guidelines'` | Current | `AuthController.login` |
| Global access-token guard | Current | `AppModule`, `AccessTokenGuard` |
| Role guard for handlers that set `@Roles` | Current | `src/roles/roles.guard.ts` |
| Double-submit CSRF, ignore GET/HEAD/OPTIONS | Current | `CsrfService`, `src/main.ts` |
| Public CSRF token | Current | `GET` on `CsrfController` |
| List and fetch staff users | Current | `GET /api/admin/all-users`, `GET /api/admin/get-user/:id` |
| Create IPW staff user (class 1, group 1364) and queue registration email | Current | `PUT /api/admin/create-user`, `UsersService.create` |
| Update staff profile and write MD5 into `users.password` | Current | `PATCH /api/admin/update-user/:id` |
| Latest 50 audit-log rows | Current as a read | `GET /api/admin/member-activity` |
| Rows from `ipw_sessions` | Current as a read. This API does not write that table | `GET /api/admin/live-users` |
| Add or remove a role for a user on the IPW group | Current | `POST /api/admin/role`, `DELETE /api/admin/role` |
| Search members | Current | `POST /api/admin/all-members` |
| Groups and roles for a chosen user | Current | `POST /api/admin/groups-and-roles` |
| Groups a user has not joined | Current | `POST /api/admin/available-groups` |
| Set member role to id 10, 11, 14, or 16 | Current | `POST /api/admin/add-member-role` |
| Add or remove group membership | Current | `POST /api/admin/add-group-membership`, `POST /api/admin/remove-group-membership` |
| Search member groups (`organisation_type_id = 0`) | Current | `POST /api/admin/all-groups`, `GroupService.findByQuery` |
| Delete a group with no `group_eval_form` rows | Current | `DELETE /api/admin/group` |
| Signed-in contact details | Current | `GET /api/user/contact-details` |
| Signed-in groups and role flags | Current | `GET /api/user/groups-and-roles` |
| Available groups, pending join, cancel pending | Current | `src/users/users.controller.ts` |
| Create a class-2 user and queue registration email | Current. No active role check | `PUT /api/user/member` |
| Update any user id and optional MD5 password | Current. Comment says it should be self-only | `PATCH /api/user/:id` |
| Delete a user | Current. System Developer or IPW Administrator | `DELETE /api/user/:id` |
| List all users, or one user | Current. Same two roles | `GET /api/user`, `GET /api/user/:id` |
| Role description lists | Current | `src/roles/roles.controller.ts` |
| Roles for one user | Current. System Developer or IPW Administrator | `GET /api/role/user/:id` |
| Report years, group names, control products, table, CSV | Current | `src/reports/` |
| English and Afrikaans CSV headers | Current | `ReportsController.csvReport` |
| Document categories, upload, own list, all-profiles, all-documents, download | Current since `f01f621` | `src/upload/` |
| Email outbox and scheduled SMTP send | Current | `src/email/email.service.ts` |
| Registration email templates (HTML and text) | Current. Copied to `dist` by `nest-cli.json` assets | `src/email/template/registration.*` |
| Sqids encoding of user id on the confirmation link | Current. Alphabet is `SQIDS_SALT` | `UsersService.encodeId` |
| Prisma client generated from the MySQL schema | Current | `src/prisma/schema.prisma`, `npm run prisma` |
| SonarQube scan on `main` | Current | `bitbucket-pipelines.yml`, `sonar-project.properties` |
| Deploy script skeleton | Current in git. Placeholders not filled | `deploy/deploy.sh` |

## Stub or scaffold features

| Feature | Status | Where |
|---|---|---|
| `POST /api/auth/reset` | Returns `{ redirect: '/reset-instructions' }` and does not change credentials | `AuthController.reset` |
| `POST /api/auth/confirm/:type/:userHash/:token` | Returns no body | `AuthController.confirmRegistration` |
| `AppController` | No routes | `src/app.controller.ts` |
| `MemberController` (`member`) | No routes | `src/member/` |
| `ManagementController` (`management`) | No routes | `src/management/` |
| `PublicController` (`public`) | No routes | `src/public/` |
| `AuditController` (`audit`) | No routes | `src/audit/` |
| `GroupController` (`group`) | No routes. `GroupService` is used by admin | `src/group/` |
| Jest `*.spec.ts` files | `should be defined` only | each `*.spec.ts` |
| E2E `GET /` expects `Hello World!` | Does not match `AppController` | `test/app.e2e-spec.ts` |
| PM2 start scripts | `package.json` names config files that are not in git | `start:production`, `start:staging`, `start:devpm2` |
| Example migration | Creates `public.demo_table` with PostgreSQL syntax | `migration/001_example__action_timestamp.sql` |
| Supabase table scripts | `public.code_deploy`, `public.migrations` | `deploy/code_deploy_table.sql`, `deploy/migrations_table.sql` |

## Commented or not wired

| Feature | Evidence |
|---|---|
| Express session and `express-mysql-session` | Commented in `src/main.ts`, with a TODO to remove the packages. Packages remain in `package.json` |
| CASL ability checks | `@casl/ability`, `@casl/prisma`, `@casl/vue`, `nest-casl` in `package.json`. No reference under `src/` |
| User-class guard | `src/users/users.guard.ts` and `@UserClasses`. Not registered in `AppModule` |
| Role annotations on most `/api/user` reads and on join-group | Commented in `UsersController` |
| `GET /api/user/username-exsits` (spelling in the comment) | Commented |
| Select profile / selected profile / clear selected profile | Commented. Refers to `user_selected_group`, which is not in the Prisma schema |
| `POST /api/user` create | Commented. Would have required Member Administrator and passed `create(..., true)` |
| Public confirmation GETs | Commented in `src/public/public.service.ts` |
| Password-reset and email-change confirm routes | Commented in `AuthController` |
| `forced_password_update` on `user_password` | Column exists. `AuthService` comments say the force-update flag is not used |
| Audit log on group delete | PHP-style call left as comments in `GroupService.remove` |
| Suspension handling on admin user update | Comment only in `AdminController.update` |
| Reminder email template | Files exist. No caller passes `template: 'reminder'` |
| Logger middleware | Commented in `AppModule` |
| CSRF interceptor `ClearCsrfInterceptor` | Import path commented in `AppModule` |
| `EmailService.verify()` on startup | Commented in the constructor |

## Schema present, no write API in this revision

These models are in `src/prisma/schema.prisma`. This Nest code does not insert or update them, except where a feature above reads them.

| Area | Models |
|---|---|
| Evaluation forms | `eval_form`, `eval_form_item`, `eval_form_part`, `eval_form_part_item`, `eval_template`, `eval_template_item`, `group_eval_form` and its item, part-item, appendix, attachment, farms, notes |
| Audits | `group_audit`, `ipw_audit`, `ipw_audit_inspector`, `auditlog` (read only) |
| Probation | `group_probation` |
| SAWIS | `sawis_compliance`, `sawis_farm`, `user_group_sn` (read by group search) |
| Content | `content_email`, `content_menu`, `content_page_item`, `htmlpage`, `htmlpage_file`, `language_set`, `language_set_item` |
| Membership catalogue | `user_group_membership`, `organisation_type` |
| Analysis files | `user_group_analysis_results` |
| Legacy sessions | `ipw_sessions` (read), `admin_sessions` (`@@ignore`) |
| RBAC catalogue | `rbac_actions`, `rbac_domains`, `rbac_objects`, `rbac_privileges` and the link tables. Runtime checks use role names, not these privilege rows |
| Ignored scratch table | `tmptbl` |

`group_eval_form.status` labels are **implemented** only inside `UsersService.getAvailableProfiles`. Reports filter statuses 50 and 55. Nothing in this API changes a form status.

## Changed on the commits after the split

| Change | Commit | Date |
|---|---|---|
| Repository created from a server split; pipeline and README added | `9e12c3d` through `71576ec` | 2025-05-26 |
| Deploy script, Supabase SQL, example migration | `9f07dec` | 2025-05-27 |
| Password update path adjusted so sign-in can use the updated password | `3931ee7` | 2025-06-02 |
| Upload and download, login by username or email, last logon | `f01f621` | 2025-06-03 |

`updatePassword` at `f5ff052` still writes MD5 to `users.password` and does not replace `user_password`. Login tries argon2 first and falls through to MD5 when argon2 does not verify. That is the behaviour at the tip, including after `3931ee7`.

## Planned only as comments

Not scheduled in a tracker. They are TODOs in source:

- Enforce member-admin or IPW-admin on `PUT /api/user/member`.
- Limit `PATCH /api/user/:id` to the caller.
- Member admins need a limited role route (`RolesController` comment).
- Use `forced_password_update`.
- Write an audit row when a group is deleted.
- Notify an admin when email sending hits a fatal error.
- Clear the email `processing` flag after a successful batch (the clear call is commented).
- Remove unused session packages.
- README deploy section says schedule the script every 15 minutes. The script itself loops without sleeping.

## Removed

No feature-removal commit is in the nine-commit history. Session support and several user routes are commented in the initial tree rather than deleted in a later commit. The parent codebase before `1aafc59` is not in this repository, so earlier removals are **Unknown**.
