# PRD — logintuto

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/logintuto.git`, `master` at `f679256` (2020-04-02).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the PHP, JavaScript, and git history on that revision. Provenance is labelled. The SMTP password stored in `php/email.php` is omitted.

## Product purpose

**Inferred.** A person can create an account, log in with a username and password, land on a dashboard, change profile fields, reset a forgotten password by email, and an operator can name user groups.

**Unknown / Requires Confirmation:** who the intended user is, whether this was a training exercise (the repository is named `logintuto`), and whether it is still used. No README states a purpose. The last commit is 2020-04-02.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Person signing up | `signUpForm.php` posts to `php/signUp.php`, which inserts `user` | **Implemented**. No role is assigned. `refUserGroupID` is not written |
| Person logging in | `index.php` posts to `php/login.php` | **Implemented**. Success stores `$_SESSION['username']`, `$_SESSION['id']`, and `$_SESSION['userGroup']` |
| Person already in a session | `index.php` sends `Location: dashboard.php` when `$_SESSION['username']` is set | **Implemented** |
| Person on the dashboard | `dashboard.php` | **Implemented** as a page. The page does not test the session before rendering |
| Person editing their profile | `profileform.php` posts to `php/profile.php` | **Implemented**. The script reads `$_SESSION['username']` and `$_SESSION['id']` and does not reject a missing session |
| Person recovering a password | `recover.php` and `newPassword.php` | **Implemented**. The reset URL is not gated by a session |
| Person logging out | `php/logout.php` | **Implemented** only when `$_SESSION['username']` is set |
| User group | `$_SESSION['userGroup']` is set from `user.refUserGroupID` | **Implemented** as stored data. No page reads it to allow or deny an action. A check in `dashboard.php` is commented out |
| Group-name editor | `usergroup.php` | **Implemented**. The dashboard does not link to this page. No login check |

No permission table is enforced. Commented code in `php/usergroups.php` and `usergroup.php` names `blnRead`, `blnWrite`, and `blnNoAccess`. Those flags are not updated by the live request.

## Systems the product talks to

1. MySQL through `mysqli`. Host `localhost`, user `root`, empty password, database `login` (`php/db.php`). Tables named in SQL: `user`, `tblusergroup`, `tblloginuser`, `loguser`.
2. Gmail SMTP through PHPMailer 6.1.4: host `smtp.gmail.com`, port `587`, TLS, SMTP auth on, `SMTPDebug` set to `SMTP::DEBUG_SERVER` (`php/email.php`).
3. The browser, for jQuery Ajax and the checks in `js/validation.js`.
4. Public CDNs on some pages: jQuery 3.4.1 (`profileform.php`, `usergroup.php`, `php/resetPassword.php`) and Bootstrap 4.4.1 JS (`usergroup.php`, `php/resetPassword.php`).
5. Google Fonts: Open Sans is linked from `dashboard.php` and `profileform.php`.

No queue, cache, object store, or payment provider is present.

## Major workflows

### WF-1 — Open the login page

**Implemented** in `index.php`:

1. `session_start()`.
2. If `$_SESSION['username']` is set, send `Location: dashboard.php`.
3. Otherwise render the login form. The username control is `type="email"` with placeholder `username`. Submit is `type="button"`, so the browser's email constraint is not what posts the form. The click handler posts any non-empty pair.

### WF-2 — Log in

**Implemented** in `index.php` (client) and `php/login.php` (server):

1. Empty username or password: the page shows "All fields are required" and does not call the server.
2. Otherwise Ajax POST `userName` and `passWord` to `./php/login.php`.
3. The server selects `user` where `username` and `password` equal the posted strings. The password is not hashed.
4. If a row is fetched and the `if ($row > 0)` test passes, the script starts a session, stores username, `id`, and `refUserGroupID`, sets `last_loggedin` to `NOW()` for that username, inserts `tblloginuser` (`strResult` = `Login successful`, `strUsername` = the username), and echoes `1`.
5. The browser treats a response equal to `1` as success and navigates to `dashboard.php`.
6. Otherwise the server looks up the username alone. It inserts either `Login failed : Incorrect password` or `Login failed : User not found`, then echoes `0`. The browser clears both fields and shows "Password or Username is incorrect".

`if ($row > 0)` compares the array from `mysqli_fetch_array` to an integer. What that comparison does depends on the PHP version. **Unknown / Requires Confirmation** which PHP version this host uses. The same comparison appears in `SaveLog`.

A failed `mysqli_query` on the first select produces no echo. The browser then takes the failure branch.

### WF-3 — Sign up

**Implemented** in `signUpForm.php` and `php/signUp.php`:

1. Empty name, email, password, or confirmation: "All fields are required". No server call.
2. `emailValidation` rejects the address: "Invalid email address".
3. The two passwords differ: "Passwords should be the same".
4. `passwordValidation` fails: "Make sure your password is strong".
5. Otherwise Ajax POST `userName`, `email`, and `passWord` to `./php/signUp.php`.
6. The server selects `user` where `email` or `username` matches. If `mysqli_num_rows` is greater than 0, it echoes `1` and does not insert.
7. Otherwise it echoes `0`, then `Insertdata` inserts `email`, `username`, and `password`. On a true result it sets `date_created` to `NOW()` for that username.
8. The browser alerts "This user as already an account" when the response equals `1`. Any other response, including `0` followed by a MySQL error string, shows "Your account has been created".

The insert does not set `refUserGroupID`. The confirmation password is not sent or stored.

### WF-4 — Dashboard

**Implemented** in `dashboard.php`:

1. `session_start()`, then `include 'php/login.php'`.
2. Render a sidebar: Home (`#`), Profile (`profileform.php`), Settings (`#`), Users (`#`), Help (`#`), Notifications (`#`), Logout (`./php/logout.php`).
3. The main region is an empty `div.document`.

There is no redirect when the session is empty. The commented block would set a hide style when `$usergroup = 1`. That expression assigns `1`; it is not executed.

### WF-5 — Update profile details

**Implemented** in `profileform.php` and `php/profile.php`:

1. The page includes `dashboard.php` first, so the HTTP body starts with the dashboard HTML.
2. Empty new username or email: "All fields are required".
3. Invalid email: the script writes "Invalid email address" into `#error_message`. The markup uses the class `error_message`, not that id.
4. Otherwise POST `newUserName` and `email` to `./php/profile.php`.
5. The server loads the row for `$_SESSION['id']` and reads its password.
6. If the new username equals `$_SESSION['username']`, it calls `SaveLog` with log key `email`, then `updatetUser`, then sets `last_modified`.
7. If the username differs and `updatetUser` returns true, it calls `SaveLog` with log key `username`, replaces `$_SESSION['username']`, and sets `last_modified`.
8. `updatetUser` echoes `1` on a successful query. The browser alerts `success` only when the response equals `1`.

`updatetUser` updates `username` and `email` where `username` equals the old session username. It does not check that the new email or username is unused.

### WF-6 — Change password while logged in

**Implemented** on the same profile page:

1. Empty old password, new password, or confirmation: "All fields are required". The selectors `#newpwrd1` and `#newpwrd2` are not the ids on the inputs (`pass1` and `pass2`).
2. Mismatch: "Passwords should be the same".
3. `passwordValidation` fails: "Make sure your password is strong".
4. Otherwise POST `oldPassWord` and `NewPassWord` to `./php/profile.php`.
5. The server calls `SaveLog($NewPassword, 'password')` before it checks the old password. `SaveLog` expects four arguments (`username`, `email`, `password`, `key`).
6. It selects `user` where `password` and `username` match the old password and the session username.
7. If `mysqli_num_rows` is greater than 0, it echoes `1`, updates `password`, and sets `last_modified`. Otherwise it echoes `0`.
8. The browser alerts "This user as already an account" when the response equals `1`, and alerts `success` otherwise.

The success and failure messages on this button are the reverse of the server echoes.

### WF-7 — Email a reset link

**Implemented** in `recover.php` and `php/email.php`:

1. Empty email: red border, and the script writes into `#error`, which is not in this page.
2. `emailValidation` fails: "Invalid email".
3. Otherwise POST `email` to `./php/email.php`.
4. If no POST `email`, `email.php` calls `redirectToLoginPage()` (`Location: ../index.php`).
5. If no `user` row has that email, the script echoes `user not found`.
6. If a row exists, it sets `token` to 40 hex characters (`bin2hex(random_bytes(20))`) and `tokenExpire` to `NOW()` plus 20 minutes, for every row with that email. It echoes `success` before it sends mail.
7. PHPMailer sends an HTML message, subject `Reset Password`, display name `Consolate`. The link is `http://localhost/logintuto/newPassword.php?email=<email>&token=<token>`.
8. On send it echoes `Message has been sent` and `Check your inbox an email has been sent`. On exception it echoes the PHPMailer error. `SMTP::DEBUG_SERVER` also writes the SMTP dialogue into the same response.
9. The browser treats the call as success only when `response.indexOf('success')` is truthy. A response that begins with `success` yields index `0`, which is falsy, so the page shows "No user is registered with this email address!" unless a later part of the response contains `success` at a non-zero index.

### WF-8 — Set a new password from the link

**Implemented** in `newPassword.php` and `php/reset_pass.php`:

1. The page reads `$_GET['email']` and `$_GET['token']` into the Ajax payload.
2. Empty passwords: "All fields are required".
3. Mismatch: "passwords should be the same!".
4. `passwordValidation` fails: "Make sure your password is strong". The border selectors are `#password1` and `#password2`. The inputs are `#pass1` and `#pass2`.
5. POST `email`, `token`, and `passWord` to `./php/reset_pass.php`.
6. The server selects `user.id` where `email` and `token` match, `token` is not empty, and `tokenExpire > NOW()`.
7. If that row test passes, it sets `token` to empty, sets `password` to the posted value, sets `last_modified` to `NOW()`, and echoes `1`. A failed update `die`s with `Database query failed.`
8. Otherwise it echoes `0`.
9. The browser uses `response.indexOf(1)`. On the body `1`, `indexOf` returns `0` (falsy) and the page shows the failure text. On the body `0`, `indexOf` returns `-1` (truthy) and the page says the password was updated, then opens `index.php`.

### WF-9 — User groups

**Implemented** in `usergroup.php` and `php/usergroups.php`. No navigation link points at `usergroup.php`.

1. The page selects every `tblusergroup` row and prints `GroupID` and an editable `strName`.
2. Add with an empty name only paints the input border red.
3. Add with a name POSTs `newGroup`. The server inserts `strName` and echoes `1` or `0`. The browser alerts `success` when the response equals `1`.
4. Edit POSTs `GroupID`, `blnUpdate` = `1`, and `strName`. The server echoes the UPDATE statement, then `1` or an error string. Because the body is not exactly `1`, the browser alerts "Oops,something wrong happened" after a query that succeeded.

Commented checkbox logic for `blnRead`, `blnWrite`, and `blnNoAccess` is not in the live request.

### WF-10 — Log out

**Implemented** in `php/logout.php`:

1. If `$_SESSION['username']` is set, unset that key, `session_destroy()`, and redirect to `../index.php`.
2. If it is not set, the script ends with an empty body.

`welcome.php` is a separate page that prints `Welcome` plus `$_SESSION['username']` and links to `./php/logout.php`. Nothing in the other pages links to `welcome.php`.

## Functional requirements

### FR-001 — Account creation

The system stores a username, email, and password on `user` when the posted email and username are both absent, then stamps `date_created`.

- Actor: anonymous visitor on `signUpForm.php`.
- Trigger: Sign Up click after the client checks pass.
- Inputs: `userName`, `email`, `passWord`.
- Output: echo `1` if a row already matches; otherwise echo `0` and insert.
- Validation: client only, except the duplicate query. The server does not re-check email shape or password strength.
- Error: duplicate echo `1`. Insert failure still leaves the leading `0` in the response, and the browser shows the created message.
- Dependency: table `user`, `php/db.php`.
- Provenance: **Implemented**.

### FR-002 — Login session

A matching username and password creates a PHP session and records the attempt.

- Actor: visitor on `index.php`.
- Inputs: `userName`, `passWord`. Compared to `user.username` and `user.password` as plain text.
- Output: echo `1` or `0`. Session keys `username`, `id`, `userGroup`. Column `last_loggedin` set to `NOW()`.
- Side effect: insert into `tblloginuser` (`strResult`, `strUsername`) for both outcomes, when the insert runs.
- Provenance: **Implemented**. Hashing is absent. **Inferred** that this was unintentional; the code never calls `password_hash`, `password_verify`, or `md5`.

### FR-003 — Return visit

A browser that already has `$_SESSION['username']` is sent from `index.php` to `dashboard.php`.

- Provenance: **Implemented**.

### FR-004 — Dashboard shell

After login the browser opens `dashboard.php`, which shows Profile and Logout as working links and Home, Settings, Users, Help, and Notifications as `href="#"`.

- Provenance: **Implemented**. A requirement that those items open screens was not found.

### FR-005 — Profile identity update

The logged-in username can be replaced, and the email can be replaced, on the row whose username equals the session username. A `loguser` row is written.

- Inputs: `newUserName`, `email`.
- Log key: `email` when the username is unchanged; `username` when it changes.
- Provenance: **Implemented**. Uniqueness of the new values is not checked.

### FR-006 — In-session password change

The password changes when the posted old password equals the stored password for the session username.

- Inputs: `oldPassWord`, `NewPassWord`.
- The server echoes `1` on match. The profile page's success alert is shown on the other branch.
- Provenance: **Implemented** server update. The alert text does not match the echo. **Requires Confirmation** if the alert copy was a mistake.

### FR-007 — Reset token

A known email receives a token that expires 20 minutes after `NOW()`, and a message whose link targets `http://localhost/logintuto/newPassword.php`.

- Provenance: **Implemented**. The host name is a literal. Whether production used another host is **Unknown / Requires Confirmation**.

### FR-008 — Consume the token

A posted email and token, with `tokenExpire` still in the future and `token` not empty, replace `password`, clear `token`, and set `last_modified`.

- Provenance: **Implemented** in `php/reset_pass.php`. The page's reading of echo `1` versus `0` does not follow those echoes. See WF-8.

### FR-009 — User group names

A visitor who opens `usergroup.php` can list group names, insert `strName`, and update `strName` by `GroupID`.

- Provenance: **Implemented**. No requirement text says who may do this. The page is unlinked and unauthenticated.

### FR-010 — Logout

Logout destroys the session and returns to `index.php` when a username is in the session.

- Provenance: **Implemented**.

### FR-011 — Login and profile audit rows

Successful and failed logins insert `tblloginuser`. Profile saves insert `loguser` with JSON `log_oldData` and `log_newData` (username, email, password) plus `log_key` and `timestamp`.

- Provenance: **Implemented**. No screen reads these tables back.

## Constraints

- Passwords are stored and compared as the posted string. **Implemented**.
- Sign-up, reset, and profile password changes apply `passwordValidation` only in the browser. **Implemented**.
- SQL is built by concatenating request values in `php/login.php`, `php/signUp.php`, `php/email.php`, `php/reset_pass.php`, `php/profile.php`, `php/usergroups.php`, and `php/functions.php`. **Implemented**.
- `dashboard.php`, `profileform.php`, `usergroup.php`, `php/profile.php`, and `php/usergroups.php` do not refuse an anonymous caller. **Implemented**.
- The reset link host, the database name, and the SMTP account are literals. **Implemented**.

## Discrepancies

| Topic | What the screens suggest | What the code does |
|---|---|---|
| Profile password result | Button is "Change password" | Echo `1` alerts "This user as already an account". Echo `0` alerts `success` |
| Recover result | Copy says an email was sent | `indexOf('success')` is falsy when the body starts with `success` |
| Reset result | Copy says the password was updated | `indexOf(1)` is falsy for body `1` and truthy for body `0` |
| Group rename | Alert `success` is coded for response `== 1` | The server prints the SQL first, so the alert is the error text |
| Users menu | Sidebar item "Users" | `href="#"`. `usergroup.php` exists and is not linked |
| Group permissions | Commented read / write / no-access controls | Live update changes `strName` only |
| Login field | `type="email"` and an envelope icon | The server matches `user.username`, not `user.email` |
| Password strength | "atleast 6 characters" is shown by `passwordCheck` | `passwordValidation` does not test length. The sign-up gate is the character classes only |
| `functions.php` comment | "email function still need to be added" | `php/email.php` sends mail |
| Second dashboard | `dashboard2.php` | Stylesheet `css/dashbaord.css` is missing. Page is unlinked |

## Out of scope in this tree

No documented backlog file exists. The following are absent, not scheduled:

- A schema or seed.
- Password hashing.
- Server-side copies of the email and password checks.
- Enforcement of `refUserGroupID` or the commented access flags.
- A reader for `loguser` or `tblloginuser`.
- Tests, CI, and a deployment manifest.
- Links for Settings, Help, and Notifications.
