# PRD — userModule

Evidence date: 2026-09-29.

Source: `git@bitbucket.org:overdrive/usermodule.git`, `master` at `a8971a4` (2019-06-24).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `README.md` and the PHP, JavaScript, and git history on that revision. Provenance is labelled. The database password stored for host `lamp` is omitted.

## Product purpose

**Documented.** `README.md` titles the work "Unity : userModule" and lists four phases: a validated user form that writes to MySQL, a table of users that can be edited, a catalogue of user types, and a login that stores an MD5 password.

**Implemented.** The running pages create, list, update, and delete rows in a table named `` `user` ``, and a login page compares an email and an MD5 digest. They do not implement the type catalogue, the `api/` post described in phase 1, or a session.

**Unknown / Requires Confirmation:** who the intended operator is, whether this module was embedded in another Overdrive system, and whether it is still used. The last commit is 2019-06-24.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Person registering | `login.php` links to `registrationForm.php`. The registration page inserts a row | **Implemented**. The README does not name this actor |
| Person using the home form | `userForm.php` inserts the same kind of row and links to `editUser.php` | **Implemented**. No login check |
| Person editing | `editUser.php` lists every row and submits update or delete | **Implemented**. No login check |
| Person logging in | `login.php` posts email and password | **Implemented**. Success does not create a session |
| User type on the row | Stored labels `Administrator`, `Developer`, `Technical user` | **Implemented** as data. The label is not checked before any action |
| README roles "Admin, Developer, Technical User" | Phase 1 dropdown wording | **Documented**. The screens use different strings. See discrepancies |

No permission table, group, or `$_SESSION` appears in the application PHP.

## Systems the product talks to

1. MySQL through `mysqli`, database name `usermodule`, table `` `user` ``. Connection parameters depend on `$_SERVER["SERVER_NAME"]`.
2. Public CDNs for Bootstrap 4.3.1, jQuery 3.4.0, and Popper 1.14.7.
3. The browser, for `alert()` messages and for `clientsideUserFormValidation.js`.

No mail transport, HTTP API client, queue, or file store is present. The Smart Admin template under `template/` includes its own demo pages (mail, maps, invoices). Those pages are not called by the PHP.

## Major workflows

### WF-1 — Open a page and connect

**Implemented** in `dbconnection.php`, included by all four screens:

1. Include `projectFunction.php`.
2. If `SERVER_NAME` is `localhost`, connect to `localhost` as `root` with an empty password and database `usermodule`.
3. If `SERVER_NAME` is `lamp`, connect to host `lamp` as `root` with the password committed in that file and database `usermodule`.
4. Any other server name leaves host, user, password, and database as empty strings.
5. On `connect_error`, run `alert('Database Connection failed')` and `die` with the driver error text.
6. The success alert is commented out.

`print_rr` is never called.

### WF-2 — Register or create a user

**Documented** phase 1: validate, then GET the fields and POST only the ID number to PHP under `api/`.

**Implemented** on both `registrationForm.php` and `userForm.php`, which duplicate the same insert. There is no `api/` directory.

1. The page renders today's date in a disabled field. The server does not read that field. It sets `txtUserDateCreated` to `date("Y-m-d")` when submit is posted.
2. Submit is `method="post"`. The button calls `validation()` and cancels the post when that function returns false.
3. On POST `submit`, the script escapes first name, last name, ID number, date of birth, user type, email, gender, and notes. It sets the password to `md5` of the escaped password. Confirm password is not stored.
4. Status is derived from whether `active` and `inactive` are empty. The second assignment overwrites the first. See FR-012.
5. If the table has at least one row, the script loads every row and compares `txtIDNumber` to the posted ID. A match alerts `Error! User ID Number already exist` and skips the insert.
6. Otherwise it prepares an 11-column insert. Success alerts `User details successfully inserted`. Prepare failure alerts `Error! User details were not inserted`. `mysqli_stmt_execute` failure is not checked.
7. The page then renders one HTML table row from the variables just posted. It does not reload the table from MySQL. `registrationForm.php` omits the email column in that table. `userForm.php` includes it.

`registrationForm.php` always shows a nav link to `login.php`. It does not redirect after a successful insert. A commit on 2019-06-23 describes a redirect after registration. The code at HEAD does not do that.

### WF-3 — Log in

**Documented** phase 4: username, password, MD5, authentication, login screen.

**Implemented** in `login.php`:

1. The form posts `email` and `password`. A link points at `registrationForm.php`.
2. The Submit button calls `validation()`. That function reads `firstname`, `lastname`, `id_number`, `dob`, gender, status, and `confirmPassword`. Those elements are not on the login page. The call throws before it can return true or false. Whether the browser still posts the form is **Unknown / Requires Confirmation**.
3. If `submit` is posted, the script loads `SELECT * FROM `user``. Zero rows alerts `Error! Database is at empty`.
4. Each row is compared with `==` on `txtEmail` and `txtPassword`. The posted password is escaped, then passed through `md5`, before the compare. The first match sets `$correctDetails` and breaks.
5. The next line is `echo ($correctDetails) ? header("Location: userForm.php") : "<script ... alert('Incorrect login details')</script>"`.
6. The file prints HTML comments before the PHP block. With output buffering off, `header()` runs after body output has started, so the `Location` header is not sent. Whether this host enables output buffering is **Unknown / Requires Confirmation**.
7. No `session_start`, cookie, or token is set. `userForm.php` does not know that login ran.

An inactive `txtStatus` is not consulted. Any row with a matching email and digest is treated as success.

### WF-4 — List, update, and delete

**Documented** phase 2: a table of all users; click a row to edit and save.

**Implemented** in `editUser.php`:

1. The page selects every column of every `user` row and prints `intUserID`, date created, name, ID number, date of birth, email, user type, gender, status, and notes. The password column is not printed. Zero rows alerts `Error! Database is at empty`.
2. A click handler copies cells into the form. It does not copy the user-type cell (`cells[7]`). Gender `Male` checks the male radio; any other text checks female. Status `Active` checks active; any other text checks inactive.
3. The form uses `method="get"`. Update calls `validation()` (same missing-password problem as login). Delete does not.
4. Update prepares an `UPDATE` of every stored column except `txtPassword`, with `WHERE txtIDNumber = $id` interpolated into the SQL string. The ID is escaped and not quoted.
5. Delete runs `DELETE FROM `user` WHERE txtIDNumber = $id` the same way.
6. Success and failure use alerts: `User details successfully updated`, `Error! User details were not update`, `User details successfully Deleted`, `Error! User details not deleted`.

Changing the ID number in the form before Update uses the new value both in `SET` and in `WHERE`. The previous ID is not kept as the key.

## Functional requirements

### FR-001 — User form fields

**Documented** phase 1 requires first name, last name, a 13-digit ID number, male or female, a user-type dropdown (Admin, Developer, Technical User), Submit, and Cancel.

**Implemented** on the create and edit screens, with these extra fields that the README does not list: date created, date of birth, email, password, confirm password (create only), notes, Active, and Inactive. The type options are the strings `Administrator`, `Developer`, and `Technical user`.

Cancel on the create screens is `type="reset"`. Edit labels that button "Clear form".

### FR-002 — Client validation on create

**Implemented** in `validation()`:

- First name, last name, ID, date of birth, email, and password must be non-empty after trim. Notes are not in this list.
- ID must match `/^[0-9]+$/` and have length 13.
- First name **or** last name must match `/^[a-zA-Z]+$/`. One of the two is enough.
- One gender radio must be selected.
- Active or Inactive (or both) must be checked.
- Password and confirm password must be equal, and the password length must be at least 4.
- Email must match `/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/`.

Failure uses `alert` and returns false. These rules are not repeated in PHP.

### FR-003 — Persist a user row

**Implemented.** Insert columns and order:

`txtFirstname`, `txtLastname`, `txtIDNumber`, `txtDOB`, `txtGender`, `txtUserType`, `txtEmail`, `txtPassword`, `txtUserDateCreated`, `txtStatus`, `txtUserNote`.

The statement is prepared and bound as eleven strings. A commented block in `userForm.php` still names older columns `usr_firstname`, `usr_lastname`, `usr_IdNumber`, and so on. That block is not executed.

### FR-004 — ID number is unique per person

**Documented** in the 2019-06-23 commit subject: an existing ID number means the user already exists, because an ID number is unique per person.

**Implemented** as a PHP loop over `SELECT *`, only when `mysqli_num_rows` is at least 1. There is no unique index in the repository. Two different emails may share no check at all. Email uniqueness is not implemented.

### FR-005 — Show what was just entered

**Implemented** on the create pages as one `<tbody>` row filled from the POST variables. Before the first submit those variables are empty strings, so the table shows one blank row. This is not the "all users" table from phase 2.

### FR-006 — Show all users

**Implemented** only on `editUser.php`, as FR workflow WF-4. The password is not a column in the HTML table.

### FR-007 — Edit and save from a row click

**Implemented** with the gaps in WF-4: user type is not copied from the row, so a save leaves the dropdown on its default first option (`Administrator`) unless the operator changes it. Password is not on the form and is not updated.

### FR-008 — Delete a user

**Implemented** by GET `delete`, keyed on the ID number currently in the form. There is no confirm step in the page.

### FR-009 — Login screen

**Documented** phase 4 and **Implemented** as `login.php`. The identity compared is `txtEmail`, not a separate username. The README's "username" field does not exist.

### FR-010 — MD5 password

**Documented** phase 4: "encrypt (md5) the password on the post."

**Implemented** on create and on login as `md5` of the escaped string. The digest is what is stored and compared. Confirm password is checked only in JavaScript.

### FR-011 — Authentication gates the module

**Documented** phase 4: "build in a authtenticating on the user."

**Implemented behaviour:** login compares email and digest. It does not record the result, and the other pages do not read a login result. Direct requests to `userForm.php` and `editUser.php` run the same as a request after login.

This is a discrepancy between the README and the code, not a confirmed product decision to leave the pages open.

### FR-012 — User status Active or Inactive

**Implemented.** Checkboxes `active` and `inactive` are optional in HTML. The server does:

```text
statusActive = empty(active)
statusInactive = empty(inactive)
if statusActive then Active else Inactive
if statusInactive then Active else Inactive
```

The second line replaces the first. The stored value is `Active` when `inactive` was not posted, and `Inactive` when `inactive` was posted. The `active` checkbox does not affect the stored value.

The client script requires at least one box, so a normal browser submit has a selection. Both boxes can be checked; the stored value is then `Inactive`. If the post is made without JavaScript and neither box is sent, the stored value is `Active`.

`txtStatus` is not read during login.

The commit on 2019-06-18 that added a "dormant" checkbox is historical. The current forms do not contain dormant.

### FR-013 — User types as a maintained list

**Documented** phase 3: add a type, archive a type, link it to the user module.

**Not implemented.** Types are three hardcoded options. Nothing archives a type.

### FR-014 — Theme

**Documented:** "Theme include in template."

**Implemented:** `template/` is Smart Admin 2.0 (`template/index.html` title "Dashboard | Smart Admin 2.0"). Application pages set `class="smart-style-0"` and then load Bootstrap from a CDN. They do not reference `template/assets`.

## Discrepancies

| Topic | README or commit | Code at `a8971a4` |
|---|---|---|
| Post only the ID to `api/` | Phase 1 step 4 | Full-row POST in the page itself. No `api/` path in history |
| Column names | `use_id`, `use_date_created` | `intUserID`, `txtUserDateCreated`, and the `txt*` columns above |
| Type labels | Admin, Developer, Technical User | Administrator, Developer, Technical user |
| Username | Phase 4 | Email is the login name |
| Redirect after register | Commit `45ea743` subject | Nav link to login is always visible. No redirect after insert |
| Redirect after login | Comments in `login.php` | `header("Location: userForm.php")` after HTML comments have already been output |
| All-users table on the create form | Phase 2 wording is general | Only `editUser.php` lists the table |

## Error conditions

| Condition | Behaviour | Provenance |
|---|---|---|
| mysqli connect fails | Alert `Database Connection failed`, then `die` of `connect_error` | **Implemented** |
| Create, ID already in a non-empty table | Alert `Error! User ID Number already exist`, no insert | **Implemented** |
| Insert prepare fails | Alert `Error! User details were not inserted` | **Implemented** |
| Insert execute fails | No alert. The prepare branch already announced success | **Implemented** |
| Login, zero rows | Alert `Error! Database is at empty` | **Implemented** |
| Login, no email/digest match | Alert `Incorrect login details`, unless the header path interferes | **Implemented** |
| Edit list, zero rows | Alert `Error! Database is at empty` | **Implemented** |
| Update prepare fails | Alert `Error! User details were not update` | **Implemented** |
| Delete query fails | Alert `Error! User details not deleted` | **Implemented** |
| Client rule fails | `alert` with the messages in `validation()`, return false | **Implemented** on pages where the elements exist |
| Unknown `SERVER_NAME` | Empty connection fields, then the connect-fail path | **Implemented** |

Query failure (`mysqli_query` returning false) is not handled before `mysqli_num_rows`. That path is **Unknown** at runtime and will surface as a PHP warning.

## Non-functional constraints visible in the code

- Passwords are MD5 digests. **Implemented** as specified by the README. MD5 is not a current password hash. Whether that remains acceptable is **Requires Confirmation**.
- Update and delete place the ID number in the query string and interpolate it into SQL outside the placeholder list. **Implemented.**
- Database credentials are in `dbconnection.php`. **Implemented.**
- No tests, rate limits, CSRF tokens, or security headers are in the repository.

## Out of scope for this PRD

Phase 3 remains a README item with no code. It is recorded as not implemented. This document does not turn it into a new requirement.
