# Business rules — logintuto

Evidence date: 2026-10-10.

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

No business-rules document existed in the source repository or in Silicon Ark. There is no README. Every rule below is taken from code. Rules marked **Inferred** are a reading of that code, not a signed policy.

The SMTP password in `php/email.php` is omitted.

## Identity

### BR-001 — A person is a row in `user`

**Implemented.** Sign-up inserts `email`, `username`, and `password` (`php/signUp.php` via `Insertdata`). Login reads `id`, `username`, `password`, and `refUserGroupID` (`php/login.php`). Profile updates read and write `username`, `email`, and `password` (`php/profile.php`).

Columns the code also writes, with no `CREATE TABLE` in git: `last_loggedin`, `date_created`, `last_modified`, `token`, `tokenExpire`.

Types, primary keys, and uniqueness constraints in the database are **Unknown / Requires Confirmation**. The application itself enforces uniqueness only at sign-up (BR-003).

### BR-002 — Username is the login name

**Implemented.** `php/login.php` compares the posted `userName` to `user.username`. It does not compare it to `user.email`.

The login input is `type="email"`. That attribute is **Implemented** in the markup. Because submit is a button click rather than a form submit, the page does not rely on the browser's email validity check. **Inferred:** the envelope icon and the email type do not match the column that is queried.

### BR-003 — Sign-up rejects an existing email or username

**Implemented.** `php/signUp.php` selects `user` where `email` equals the posted email or `username` equals the posted name. If `mysqli_num_rows` is greater than 0, the script echoes `1` and does not insert.

Profile update and the reset path do not repeat this check. **Implemented.**

### BR-004 — A new account has no group in this application

**Implemented.** The insert column list is `email`, `username`, `password` only. `refUserGroupID` is never written by this repository.

**Unknown / Requires Confirmation** whether another system, or a database default, fills `refUserGroupID`.

## Passwords

### BR-005 — Passwords are stored as posted

**Implemented.** Insert, login comparison, profile update, and reset all use the request string in SQL. There is no call to `password_hash`, `password_verify`, or `md5`.

### BR-006 — The browser asks for a mixed password before three actions

**Implemented** in `js/validation.js`, applied from `signUpForm.php`, `newPassword.php`, and the profile change-password click:

- A digit.
- A lower-case letter.
- An upper-case letter.
- A character in the set tilde, exclamation, at, hash, dollar, percent, caret, ampersand, asterisk, hyphen, underscore, plus, equals, question, greater-than, less-than.

`passwordValidation` does not require a minimum length.

`passwordCheck` is a separate label: under 6 characters it shows "Very Weak password (should be atleast 6 characters.)"; at 6 or more with the class pattern it shows "Strong"; otherwise "Medium (should include alphabets, numbers and special characters.)". That label does not stop the Ajax call.

The server scripts do not apply either check. **Implemented.**

### BR-007 — Confirmation must match, on the client only

**Implemented.** Sign-up, reset, and profile change require the two password fields to be equal before Ajax. The server receives one password field (`passWord` or `NewPassWord`).

### BR-008 — An in-session password change requires the current password

**Implemented.** `php/profile.php` updates `password` only when a row exists with that `password` and `username` equal to `$_SESSION['username']`. It echoes `1` in that case and `0` otherwise.

The same request calls `SaveLog` with two arguments before that test. `SaveLog` is declared with four parameters. **Implemented** call shape. Whether a log row is written on a wrong old password depends on PHP's handling of the missing arguments and on `$_SESSION['id']`. **Requires Confirmation** against the PHP version in use.

The profile page alerts "This user as already an account" when the response equals `1`, and `success` otherwise. **Implemented** copy. It does not describe the password rule.

### BR-009 — A reset token lasts 20 minutes

**Implemented.** `php/email.php` sets `token` to `bin2hex(random_bytes(20))` (40 hex characters) and `tokenExpire` to `DATE_ADD(NOW(), INTERVAL 20 MINUTE)` for every `user` row with the posted email.

`php/reset_pass.php` accepts the token only when `email` matches, `token` matches, `token` is not empty, and `tokenExpire > NOW()`. On success it sets `token` to an empty string, replaces `password`, and sets `last_modified` to `NOW()`.

### BR-010 — The reset link is a fixed local URL

**Implemented.** The message body links to `http://localhost/logintuto/newPassword.php` with query parameters `email` and `token`.

**Unknown / Requires Confirmation** whether that host was only a development default.

### BR-011 — Reset mail is sent only for an email that exists

**Implemented.** If `mysqli_num_rows` is 0, `php/email.php` echoes `user not found` and does not send mail.

The recover page shows "No user is registered with this email address!" when `response.indexOf('success')` is falsy. A body that starts with the echoed word `success` makes that index `0`. **Implemented.** The on-screen rule and the server rule therefore diverge unless later debug text contains `success` at a later index.

`SMTPDebug` is `DEBUG_SERVER`, so the SMTP conversation is part of the HTTP body. **Implemented.**

## Sessions and access

### BR-012 — A successful login stores three session values

**Implemented.** `$_SESSION['username']` is the posted username. `$_SESSION['id']` is `user.id`. `$_SESSION['userGroup']` is `user.refUserGroupID`. `session_start()` in `php/login.php` runs on that success path.

### BR-013 — The login page treats a username session as already signed in

**Implemented.** `index.php` redirects to `dashboard.php` when `$_SESSION['username']` is set. It does not check `id`.

### BR-014 — The dashboard, profile, and group pages do not require a session

**Implemented.** `dashboard.php` starts a session and includes `php/login.php`, and it still renders when `username` is absent. `profileform.php` and `usergroup.php` include `dashboard.php` and add their own markup. `php/profile.php` and `php/usergroups.php` do not redirect an anonymous caller.

**Inferred** from the missing checks: any caller who can reach those URLs can post the profile and group actions. `php/profile.php` still needs `$_SESSION['username']` and `$_SESSION['id']` for its SQL to refer to a row. A missing session is not handled with a dedicated error page. The `else` branch echoes `something wrong happened` only when neither `oldPassWord` nor `newUserName` is in the POST.

### BR-015 — Logout requires a username in the session

**Implemented.** `php/logout.php` unsets `username`, destroys the session, and redirects to `../index.php` only inside `if (isset($_SESSION['username']))`. Otherwise the response is empty.

### BR-016 — User group is stored and not enforced

**Implemented.** Login copies `refUserGroupID` into the session. No live branch reads `$_SESSION['userGroup']`.

The commented dashboard condition is `if ($usergroup = 1)`, which is an assignment. It is not executed. **Documented** only as a comment.

Commented SQL would set `blnRead`, `blnWrite`, and `blnNoAccess`. The live update sets `strName` only. **Implemented** versus **Documented** in comments. Whether those flags were a real permission policy is **Unknown / Requires Confirmation**.

### BR-017 — Group names can be created and renamed without a role check

**Implemented.** Insert requires a non-empty `newGroup` on the client. The server inserts `strName` whenever `newGroup` is posted. Rename runs when `blnUpdate` is posted, using `GroupID` and `strName` in the SQL text.

The rename response echoes that SQL before `1`. The browser's `response == 1` test fails, so the operator sees the error alert after a successful statement. **Implemented.**

## Audit stamps

### BR-018 — Login writes an attempt row and a last-login time

**Implemented.** On a match, `strResult` is `Login successful` and `last_loggedin` becomes `NOW()`. On a miss, `strResult` is `Login failed : Incorrect password` if the username exists, otherwise `Login failed : User not found`. Both insert `strUsername`.

If the first `mysqli_query` fails, the script does not insert and does not echo. **Implemented.**

### BR-019 — Profile changes write `loguser`

**Implemented** in `SaveLog`. The insert columns are `refUser_id` (`$_SESSION['id']`), `log_key`, `log_oldData`, `log_newData`, and `timestamp` (`NOW()`). Old and new payloads are JSON objects with `username`, `email`, and `password`.

Call sites:

- Unchanged username on the details form: key `email`, after reading the current row, before `updatetUser`.
- Changed username, and `updatetUser` returned true: key `username`, then the session username is replaced.
- Password form: `SaveLog($NewPassword, 'password')` before the old-password test. See BR-008.

`SaveLog` inserts only when `if ($row > 0)` passes. `$row` is the array from `mysqli_fetch_array`. The same PHP-version question as login applies. **Unknown / Requires Confirmation.**

### BR-020 — `date_created` and `last_modified` are set by follow-up updates

**Implemented.**

- After a successful sign-up insert, `date_created` is set to `NOW()` where `username` matches.
- After a profile detail update, `last_modified` is set to `NOW()` for the new username.
- After a matching password change, `last_modified` is set for the session username.
- After a matching token reset, `last_modified` is set in the same UPDATE.

`updateDate` concatenates the column name and the username into SQL. Callers pass fixed column names (`last_loggedin`, `date_created`, `last_modified`).

## Validation messages that are rules of the screens

These strings are **Implemented** as client behaviour. They are not repeated on the server.

| Screen | Condition | Message |
|---|---|---|
| Login | Username or password empty | All fields are required |
| Login | Response is not `1` | Password or Username is incorrect |
| Sign up | Any of the four fields empty | All fields are required |
| Sign up | `emailValidation` false | Invalid email address |
| Sign up | Passwords differ | Passwords should be the same |
| Sign up | `passwordValidation` false | Make sure your password is strong |
| Sign up | Response equals `1` | This user as already an account |
| Sign up | Any other response | Your account has been created |
| Recover | `emailValidation` false | Invalid email |
| Profile details | Username or email empty | All fields are required |
| Profile password | Old, new, or confirm empty | All fields are required |
| Profile password | New values differ | Passwords should be the same |
| Profile password | Class check fails | Make sure your password is strong |
| Reset | Either password empty | All fields are required |
| Reset | Values differ | passwords should be the same! |
| Reset | Class check fails | Make sure your password is strong |

## Data the code assumes

No schema file is in the repository. The following names are **Inferred** as the shape the queries expect. Confirmation against a live database is required before treating them as a contract.

### `user`

`id`, `username`, `email`, `password`, `refUserGroupID`, `last_loggedin`, `date_created`, `last_modified`, `token`, `tokenExpire`.

### `tblusergroup`

`GroupID`, `strName`. Commented code also names `id`, `blnRead`, `blnWrite`, `blnNoAccess`.

### `tblloginuser`

`strResult`, `strUsername`. Other columns, if any, are unknown.

### `loguser`

`refUser_id`, `log_key`, `log_oldData`, `log_newData`, `timestamp`.

## Rules that were not found

- No lockout, attempt counter, or expiry of the login session beyond PHP's session lifetime. The lifetime is not set in this repository. **Unknown / Requires Confirmation.**
- No email verification on sign-up. The address is stored and later used only for recovery.
- No rule that assigns a group on registration.
- No rule that stops a person opening `usergroup.php`, `profile.php`, or `usergroups.php` without logging in.
- No documented retention rule for `loguser` or `tblloginuser`. Nothing deletes those rows.
