# Requirements Document

## Introduction

The CRM Authentication feature gates the existing Event CRM Contacts application behind a login wall. Both the JSON API under `/api/contacts` and the Vue single-page application become accessible only after a User has authenticated with email and password. Authentication is delivered through Laravel Sanctum's SPA mode, which uses the same first-party cookie session that the rest of the Laravel application already relies on, together with CSRF protection on state-changing requests. There is no public registration: User accounts are provisioned exclusively by an operator running an artisan command on the server. Once authenticated, every User has equal, shared access to every Contact — there is no per-user ownership of Contacts in this iteration.

### Non-goals

- Public self-service registration and password reset flows.
- Per-user Contact ownership, multi-tenancy, role-based authorisation, or any per-user activity log.
- Two-factor authentication, magic-link login, OAuth, and social sign-on.
- Email verification (the application has no SMTP setup).
- "Remember me" cookies and trusted-device flows.

## Glossary

- **API**: The Laravel JSON HTTP interface served under the `/api` URL prefix, including the existing `/api/contacts` endpoints and the new `/api/login`, `/api/logout`, and `/api/user` endpoints introduced by this feature.
- **Auth_API**: The subset of the API that exposes the authentication endpoints `POST /api/login`, `POST /api/logout`, and `GET /api/user`.
- **Auth_Session**: The first-party, server-side session record (stored in the `sessions` table) that, in combination with the Session_Cookie, identifies an Authenticated_User across requests.
- **Authenticated_User**: A request context in which a valid Auth_Session resolves to exactly one persisted User; a request without such a session is anonymous.
- **Backend**: The Laravel 13 application running on PHP 8.3 that hosts the API and persists data to the configured MySQL database.
- **Contact_API**: The existing `/api/contacts` resource endpoints defined by the Event CRM Contacts feature.
- **Credentials**: The pair `{email, password}` supplied by a User on the Login_Form.
- **CSRF_Cookie**: The `XSRF-TOKEN` cookie issued by Laravel Sanctum's `GET /sanctum/csrf-cookie` endpoint, whose value the Frontend echoes in the `X-XSRF-TOKEN` request header on every state-changing API call.
- **Frontend**: The Vue 3 single-page application bundled by Vite and served from `resources/js`.
- **Login_Form**: The Vue component on the Login_View that accepts an email and password and submits them to `POST /api/login`.
- **Login_View**: The Vue route at `/login` that hosts the Login_Form for anonymous Users.
- **Login_Throttle**: The Backend's per-email and per-IP rate limit applied to `POST /api/login` to bound failed-login attempt rate.
- **Provisioning_Command**: The artisan command `crm:invite {email} [--password=]` that creates a new User or resets an existing User's password.
- **Sanctum**: The Laravel Sanctum package (`laravel/sanctum`) operating in SPA cookie/session mode (no API tokens are issued by this feature).
- **Session_Cookie**: The HTTP-only, same-site cookie issued by Laravel that references the Auth_Session.
- **Session_Lifetime**: The maximum idle duration of an Auth_Session before the Backend treats it as expired.
- **System**: The combined Backend and Frontend acting together to deliver the CRM Authentication feature.
- **User**: A row in the `users` table with `id`, `name`, `email`, `password`, `created_at`, and `updated_at`. The `password` column always holds a hash, never plaintext.
- **User_Resource**: The Backend's JSON serialisation of a User, containing only `id`, `name`, `email`, `created_at`, and `updated_at`.
- **Validation_Error**: A JSON response with HTTP status 422 whose body has shape `{ "message": string, "errors": { fieldName: string[] } }`, produced by the Backend when input fails validation.

## Requirements

### Requirement 1: User Account Data Model

**User Story:** As an operator, I want each User account to store only what is needed to authenticate, so that the application keeps personal data minimal and passwords safe.

#### Acceptance Criteria

1. THE Backend SHALL persist each User with the fields `id`, `name`, `email`, `password`, `created_at`, and `updated_at`, where `id` is a server-generated unique identifier that is immutable after creation and is never null or empty.
2. THE Backend SHALL store `email` as a required string of 1 to 254 characters that, after lower-casing and trimming leading and trailing whitespace, matches the format `local-part@domain` with exactly one `@` separator, a non-empty local-part, and a domain containing at least one dot and no whitespace.
3. THE Backend SHALL enforce that the lower-cased trimmed `email` value is unique across the `users` table.
4. THE Backend SHALL store `name` as a required string of 1 to 120 characters after trimming leading and trailing whitespace.
5. THE Backend SHALL store `password` as the output of the configured Laravel password hashing algorithm (bcrypt by default), as a non-empty string, and SHALL NOT store the plaintext password in any column, log, cache, queue payload, or session entry.
6. WHEN the Backend inserts a User row, THE Backend SHALL set `created_at` and `updated_at` to the same server time, stored as UTC timestamps.
7. WHEN the Backend updates any persisted field of a User other than `id` and `created_at`, THE Backend SHALL set `updated_at` to the current server time stored as a UTC timestamp and SHALL leave `created_at` unchanged.
8. WHEN the Backend serialises a User into a User_Resource, THE Backend SHALL include only the fields `id`, `name`, `email`, `created_at`, and `updated_at`, and SHALL NOT include the `password`, `remember_token`, or any other authentication secret.
9. IF an attempt is made to persist a User whose `email`, after lower-casing and trimming, equals the lower-cased trimmed `email` of an existing User, THEN THE Backend SHALL reject the operation, return a Validation_Error keyed on `email`, and leave the persisted state of every User unchanged.
10. IF an attempt is made to persist a User whose `email` value, after lower-casing and trimming, fails the format defined in criterion 2 or has length outside 1 to 254 characters, THEN THE Backend SHALL reject the operation, return a Validation_Error keyed on `email`, and leave the persisted state of every User unchanged.
11. IF an attempt is made to persist a User whose `name`, after trimming leading and trailing whitespace, has length outside 1 to 120 characters or is null, THEN THE Backend SHALL reject the operation, return a Validation_Error keyed on `name`, and leave the persisted state of every User unchanged.
12. IF an attempt is made to persist a User whose `password` value is null, empty, or not the output of the configured Laravel password hashing algorithm, THEN THE Backend SHALL reject the operation, return a Validation_Error keyed on `password`, and leave the persisted state of every User unchanged.

### Requirement 2: User Provisioning via Artisan Command

**User Story:** As an operator, I want to provision User accounts from the server's command line, so that no public registration page exists and account creation stays controlled.

#### Acceptance Criteria

1. THE Backend SHALL register a console command named `crm:invite` that accepts a required `email` argument and an optional `--password` option.
2. WHEN the operator runs `php artisan crm:invite <email>` and `<email>`, after lower-casing and trimming, satisfies the format rule in Requirement 1.2, AND no User with that lower-cased trimmed `email` exists, THE Provisioning_Command SHALL create a new User whose `email` equals the lower-cased trimmed value, whose `name` equals the local-part of the lower-cased trimmed `email` (truncated to 120 characters if longer), and whose `password` equals the hashed value of either the supplied `--password` or, when `--password` is omitted, a Provisioning_Command-generated random password per criterion 8, then SHALL exit with status code 0.
3. WHEN the operator runs `php artisan crm:invite <email>` and a User with the lower-cased trimmed `email` already exists, THE Provisioning_Command SHALL update only that User's `password` to the hashed value of either the supplied `--password` or, when `--password` is omitted, a Provisioning_Command-generated random password per criterion 8, SHALL leave the User's `id`, `name`, `email`, and `created_at` unchanged, then SHALL exit with status code 0.
4. WHEN the Provisioning_Command generates a random password because `--password` was omitted, THE Provisioning_Command SHALL print the generated plaintext password to standard output exactly once on a line by itself and SHALL NOT write the plaintext password to any log file, queue payload, or persistent storage location.
5. WHEN the Provisioning_Command receives a `--password` value, THE Provisioning_Command SHALL NOT print, log, or otherwise echo the supplied plaintext password to standard output, standard error, or any log destination.
6. IF the supplied `email` argument fails the format rule in Requirement 1.2 after lower-casing and trimming, THEN THE Provisioning_Command SHALL print an error message identifying the `email` argument to standard error, SHALL exit with status code 1, and SHALL NOT create or modify any User.
7. IF the supplied `--password` value contains fewer than 8 characters or more than 128 characters, THEN THE Provisioning_Command SHALL print an error message identifying the `--password` option to standard error, SHALL exit with status code 1, and SHALL NOT create or modify any User.
8. WHEN the Provisioning_Command needs to generate a random password, THE Provisioning_Command SHALL produce a string of 16 to 32 characters drawn from a cryptographically secure source containing at least one lowercase letter, at least one uppercase letter, and at least one digit.
9. IF a runtime, database, or hashing failure prevents the Provisioning_Command from creating or updating a User, THEN THE Provisioning_Command SHALL print an error message to standard error, SHALL exit with status code 2, SHALL NOT leave a partially-written User row, and SHALL NOT include any plaintext password value in the error message.

### Requirement 3: Sanctum SPA Authentication Configuration

**User Story:** As a developer, I want Sanctum's SPA cookie/session mode to be the only authentication mechanism for the API, so that the Frontend uses the same first-party session as the rest of the application.

#### Acceptance Criteria

1. THE Backend SHALL include the Laravel Sanctum package and SHALL configure the `auth:sanctum` guard to authenticate requests using the existing `web` session guard (cookie/session driver), with no API token guard, personal access token, or bearer-token mechanism enabled by this feature.
2. WHEN the Backend receives a request to `GET /sanctum/csrf-cookie`, THE Backend SHALL respond with HTTP status 204, set the CSRF_Cookie on the response with `HttpOnly` set to false, `SameSite` set to `lax` or stricter, and root path scope, and SHALL NOT require an authenticated session for this endpoint.
3. THE Backend SHALL configure Sanctum's stateful domain list from environment-driven configuration to include the host that serves the Frontend, where a request qualifies as a stateful request if and only if the `Origin` or `Referer` host of the request matches an entry in the configured stateful-domain list and the request carries the Session_Cookie.
4. IF the Backend receives a `POST`, `PUT`, `PATCH`, or `DELETE` request to any path under `/api/` that qualifies as a stateful request per criterion 3 AND the request does not carry a CSRF token whose value matches the CSRF_Cookie value, THEN THE Backend SHALL respond with HTTP status 419 and a JSON body of the form `{ "message": <string> }`, SHALL NOT execute the target controller action, and SHALL NOT create, modify, or delete any persisted record.
5. WHEN the Backend receives a `POST`, `PUT`, `PATCH`, or `DELETE` request to any path under `/api/` that qualifies as a stateful request per criterion 3 AND the request carries a CSRF token whose value matches the CSRF_Cookie value, THE Backend SHALL allow the request to reach the target controller action and SHALL NOT respond with HTTP status 419 on the basis of CSRF validation.
6. THE Backend SHALL configure the Session_Cookie with `HttpOnly` set to true, `SameSite` set to `lax` or stricter, and `Secure` set to true whenever the application is served over HTTPS, and SHALL NOT make the Session_Cookie value readable from JavaScript on the Frontend.

### Requirement 4: Login Endpoint

**User Story:** As a User, I want to exchange my email and password for an authenticated session, so that I can use the rest of the application.

#### Acceptance Criteria

1. WHEN a `POST /api/login` request is received with `Content-Type: application/json` and a JSON body `{"email": <string>, "password": <string>}` whose `email`, after lower-casing and trimming, matches a stored User and whose `password` verifies against that User's stored password hash, THE Auth_API SHALL establish an Auth_Session for that User, regenerate the session identifier, return HTTP status 200, and respond with a JSON body of shape `{ "data": <User_Resource> }`.
2. WHEN the Auth_API responds to a `POST /api/login` request with HTTP status 200, THE Auth_API SHALL set the Session_Cookie on the response so that subsequent same-origin requests from the same Frontend client are recognised as the Authenticated_User.
3. IF a `POST /api/login` request body omits `email`, contains a non-string `email`, contains an `email` that fails the format rule in Requirement 1.2, omits `password`, contains a non-string `password`, or contains a `password` whose length is outside 1 to 256 characters, THEN THE Auth_API SHALL return a Validation_Error keyed on each invalid field, SHALL NOT verify the supplied password against any stored hash, and SHALL NOT establish an Auth_Session.
4. IF a `POST /api/login` request body satisfies Requirement 4.3 but the supplied `email`, after lower-casing and trimming, does not match any stored User, OR matches a stored User but the supplied `password` does not verify against that User's stored password hash, THEN THE Auth_API SHALL return HTTP status 401 with a JSON body of the form `{ "message": <string> }` whose `message` is the same fixed string for both the no-such-email case and the wrong-password case, and SHALL NOT establish an Auth_Session.
5. WHEN the Auth_API returns HTTP status 401 from `POST /api/login`, THE response body SHALL NOT include any User_Resource fields and SHALL NOT include any indication of whether the supplied `email` matched a stored User.
6. THE Login_Throttle SHALL maintain two independent rolling-60-second counters per `POST /api/login` request: one keyed on the lower-cased trimmed `email` value and one keyed on the source IP address, where a failed attempt is any `POST /api/login` that ends in HTTP status 401 or 422, and SHALL treat the limit as exceeded when either counter reaches 5.
7. IF a `POST /api/login` request would exceed either limit defined in Requirement 4.6, THEN THE Login_Throttle SHALL respond within 1 second with HTTP status 429, a JSON body of the form `{ "message": <string> }`, and a `Retry-After` header containing an integer between 1 and 60, SHALL NOT verify the supplied password, and SHALL NOT establish an Auth_Session.
8. WHEN a `POST /api/login` request is received from a request that already carries a valid Auth_Session AND the supplied Credentials verify against a stored User per Requirement 4.1, THE Auth_API SHALL replace the existing Auth_Session with one bound to the User identified by the supplied Credentials and respond per Requirement 4.1.
9. WHEN a `POST /api/login` request is received from a request that already carries a valid Auth_Session AND the supplied Credentials fail Requirement 4.3 or Requirement 4.4, THE Auth_API SHALL respond with the corresponding Validation_Error or HTTP 401 response, SHALL NOT modify or invalidate the existing Auth_Session, and SHALL NOT alter rate-limit accounting other than incrementing the appropriate failed-attempt counters.
10. IF a `POST /api/login` request body is not valid JSON, has a `Content-Type` other than `application/json`, or decodes to a value that is not a JSON object, THEN THE Auth_API SHALL return a Validation_Error keyed on the offending input, SHALL NOT verify any password, and SHALL NOT establish an Auth_Session.

### Requirement 5: Logout Endpoint

**User Story:** As an Authenticated_User, I want a way to end my session, so that I can stop using the application on a shared device.

#### Acceptance Criteria

1. WHEN a `POST /api/logout` request is received from an Authenticated_User and the request carries a CSRF token whose value matches the CSRF_Cookie value, THE Auth_API SHALL invalidate the current Auth_Session such that no future request authenticated by that Session_Cookie resolves to any User, regenerate the session identifier, set a response directive that instructs the user agent to delete the Session_Cookie, and return HTTP status 204 with an empty response body within 5 seconds, where the HTTP status 204 SHALL be returned whenever the server-side Auth_Session has been invalidated regardless of whether the user agent honours the cookie-deletion directive.
2. IF a `POST /api/logout` request is received from an anonymous request context, THEN THE Auth_API SHALL respond with HTTP status 401 and a JSON body of the form `{ "message": <string> }` of 1 to 500 characters, and SHALL NOT modify any Auth_Session record or any User record.
3. WHEN the Auth_API returns HTTP status 204 from `POST /api/logout`, every subsequent request that carries only the now-invalidated Session_Cookie SHALL be treated as anonymous by every endpoint of the API.
4. WHEN the Auth_API processes a successful `POST /api/logout` per Requirement 5.1, THE Auth_API SHALL invalidate only the current Auth_Session and SHALL NOT modify any other Auth_Session of the same User, any Auth_Session of any other User, or any field of any User row.

### Requirement 6: Current-User Endpoint

**User Story:** As the Frontend, I want to ask the API who is currently logged in, so that I can render the right routes and the right header on page load.

#### Acceptance Criteria

1. WHEN a `GET /api/user` request is received from an Authenticated_User, THE Auth_API SHALL return HTTP status 200 with a JSON body of shape `{ "data": <User_Resource> }` whose `data` is the User_Resource serialisation defined in Requirement 1.7 of the User identified by the Auth_Session.
2. IF a `GET /api/user` request is received from an anonymous request context, THEN THE Auth_API SHALL return HTTP status 401 with a JSON body of the form `{ "message": <string> }`, SHALL NOT include any User_Resource field in the response body, and SHALL NOT establish a new Auth_Session.
3. WHEN a `GET /api/user` request is processed, THE Auth_API SHALL NOT modify any stored User field of any User, including `updated_at`.

### Requirement 7: Authentication Required for the Contact API

**User Story:** As the operator of the CRM, I want every Contact API endpoint to require authentication, so that anonymous visitors cannot read or modify Contacts.

#### Acceptance Criteria

1. IF a request is received with a path equal to `/api/contacts`, equal to `/api/contacts/{id}` for any `{id}` segment, or beginning with the prefix `/api/contacts/`, regardless of HTTP method (including GET, HEAD, POST, PUT, PATCH, and DELETE), AND the request is from an anonymous request context, THEN THE Backend SHALL return HTTP status 401 with a JSON body of the form `{ "message": <string> }`, SHALL NOT execute the Contact_API controller action, SHALL NOT create, modify, or delete any Contact record, and SHALL NOT issue an HTTP redirect response (HTTP status 3xx with a `Location` header).
2. WHEN a request is received with a path equal to `/api/contacts`, equal to `/api/contacts/{id}` for any `{id}` segment, or beginning with the prefix `/api/contacts/`, the request is from an Authenticated_User, AND the request satisfies CSRF validation per Requirement 3.4 where Requirement 3.4 applies, THE Backend SHALL execute the Contact_API controller action and SHALL return the response defined by the Event CRM Contacts feature for that endpoint without altering its HTTP status code, response body, or response headers as a result of this feature.
3. THE Contact_API SHALL apply no per-user filter to any read operation, so that every Authenticated_User SHALL be able to read every Contact persisted in the `contacts` table regardless of which User created or last updated that Contact.
4. THE Contact_API SHALL apply no per-user filter to any write operation, so that every Authenticated_User SHALL be able to create, update, or delete every Contact persisted in the `contacts` table regardless of which User created or last updated that Contact.
5. IF a request is received with a path equal to `/api/contacts`, equal to `/api/contacts/{id}` for any `{id}` segment, or beginning with the prefix `/api/contacts/`, the HTTP method is POST, PUT, PATCH, or DELETE, the request is from a stateful origin, AND the request fails CSRF validation per Requirement 3.4, THEN THE Backend SHALL return HTTP status 419 with a JSON body of the form `{ "message": <string> }`, SHALL NOT execute the Contact_API controller action, and SHALL NOT create, modify, or delete any Contact record.

### Requirement 8: Login Page in the Frontend

**User Story:** As a User, I want a dedicated login page in the Frontend, so that I can enter my credentials and reach the Contacts application.

#### Acceptance Criteria

1. THE Frontend SHALL register a public route at the path `/login` whose component renders the Login_View containing a labelled email input of `type="email"`, a labelled password input of `type="password"`, and a single submit control of `type="submit"`.
2. WHEN the Login_View is mounted, THE Frontend SHALL issue a `GET /sanctum/csrf-cookie` request before the User submits the Login_Form for the first time, so that a valid CSRF_Cookie is present when `POST /api/login` is sent.
3. WHEN the User submits the Login_Form with non-empty `email` and `password` values and no prior submission of the Login_Form is currently in flight, THE Frontend SHALL send a `POST /api/login` request whose `Content-Type` request header is `application/json`, whose body is `{"email": <trimmed lower-cased email>, "password": <password>}`, whose headers include the `X-XSRF-TOKEN` header echoing the current CSRF_Cookie value, and which is bounded by a 10-second response timeout.
4. WHILE a Login_Form submission is in flight, THE Frontend SHALL disable the submit control of the Login_Form so that the User cannot trigger a second submission for the same form.
5. IF the User submits the Login_Form with an `email` field that is empty after trimming or a `password` field that is empty, THEN THE Frontend SHALL display an inline validation message on each empty field, SHALL retain the values entered by the User, and SHALL NOT send a request to the API.
6. WHEN the API responds to a Login_Form submission with HTTP status 200, THE Frontend SHALL set the SPA auth state to "authenticated" with the returned User_Resource as its associated User, SHALL clear any prior inline error or validation message displayed on the Login_Form, SHALL navigate to the Frontend route at `/` unless a deferred target is held per Requirement 9.4 in which case THE Frontend SHALL navigate to that deferred target and SHALL clear the deferred target, and SHALL re-enable the submit control of the Login_Form.
7. IF the API responds to a Login_Form submission with HTTP status 401, THEN THE Frontend SHALL display an inline error message on the Login_Form indicating that the supplied Credentials are invalid, SHALL retain the entered `email` value, SHALL clear the entered `password` value, and SHALL re-enable the submit control of the Login_Form.
8. IF the API responds to a Login_Form submission with HTTP status 422, THEN THE Frontend SHALL display the Validation_Error messages on the corresponding fields of the Login_Form, SHALL retain the values entered by the User on non-erroring fields, and SHALL re-enable the submit control of the Login_Form.
9. IF the API responds to a Login_Form submission with HTTP status 429, THEN THE Frontend SHALL display an inline error message on the Login_Form indicating that further attempts are temporarily blocked, SHALL retain the entered `email` value, SHALL clear the entered `password` value, and SHALL re-enable the submit control of the Login_Form.
10. IF the API responds to a Login_Form submission with HTTP status 419 or any HTTP status of 500 or higher, OR the request fails due to a network error, OR no response is received within 10 seconds, THEN THE Frontend SHALL display an inline error message indicating that the request could not be completed, SHALL retain the values entered by the User, and SHALL re-enable the submit control of the Login_Form.

### Requirement 9: Frontend Route Guards

**User Story:** As an operator, I want every existing SPA route to require an authenticated session, so that anonymous visitors cannot reach the Contacts UI.

#### Acceptance Criteria

1. WHEN the Frontend is mounted for the first time within a browser tab, THE Frontend SHALL issue a `GET /api/user` request, SHALL set the SPA auth state to "authenticated" with the returned User_Resource if the response status is 200, and SHALL set the SPA auth state to "anonymous" if the response status is 401, before activating the router.
2. WHILE the SPA auth state is "anonymous" and a navigation target's route is `contacts.index`, `contacts.create`, or `contacts.edit`, THE Frontend SHALL cancel that navigation and SHALL navigate to `/login` instead.
3. WHILE the SPA auth state is "authenticated" and a navigation target's route is the Login_View at `/login`, THE Frontend SHALL cancel that navigation and SHALL navigate to `/` instead.
4. WHEN a navigation to `contacts.index`, `contacts.create`, or `contacts.edit` is cancelled per Requirement 9.2, THE Frontend SHALL store the original target's full path including query string and hash fragment as the deferred target, replacing any prior deferred target, and SHALL navigate to that deferred target on the next successful login completion per Requirement 8.6.
5. IF any API response received by the Frontend after the initial mount probe carries HTTP status 401, EXCEPT a response to `POST /api/login`, THEN THE Frontend SHALL set the SPA auth state to "anonymous", SHALL discard any locally cached User_Resource, AND THE Frontend SHALL, when the route at the time of receiving the 401 is one of `contacts.index`, `contacts.create`, or `contacts.edit`, navigate to `/login` while storing the current route's full path as a deferred target per Requirement 9.4.
6. WHEN the initial `GET /api/user` probe defined in Requirement 9.1 fails with a network error, fails with any HTTP status other than 200 or 401, or no response is received within 10 seconds, THE Frontend SHALL set the SPA auth state to "anonymous", SHALL display a non-blocking error message indicating that the connection failed, AND IF the entry route is one of `contacts.index`, `contacts.create`, or `contacts.edit`, THEN THE Frontend SHALL navigate to `/login` while storing the entry route's full path as a deferred target per Requirement 9.4, ELSE THE Frontend SHALL remain on the entry route.

### Requirement 10: Logout Control in the Frontend

**User Story:** As an Authenticated_User, I want a logout control in the Frontend header, so that I can end my session.

#### Acceptance Criteria

1. WHILE the SPA auth state is "authenticated", THE Frontend SHALL render a logout control in the application header that displays the email of the current User_Resource and whose accessible name contains "Log out".
2. WHILE the SPA auth state is "anonymous", THE Frontend SHALL NOT render the logout control in the application header.
3. WHEN the User activates the logout control, no prior logout request is currently in flight, AND a non-empty CSRF_Cookie is present, THE Frontend SHALL send a `POST /api/logout` request whose headers include the `X-XSRF-TOKEN` header echoing the current CSRF_Cookie value and which is bounded by a 10-second response timeout.
4. WHILE a logout request is in flight, THE Frontend SHALL disable the logout control so that the User cannot trigger a second logout request, and SHALL re-enable the logout control after the request completes, fails, or its 10-second timeout elapses.
5. WHEN the API responds to a logout request with HTTP status 204 or HTTP status 401, THE Frontend SHALL set the SPA auth state to "anonymous", SHALL discard any locally cached User_Resource, SHALL navigate to `/login`, and SHALL clear any deferred target stored per Requirement 9.4.
6. IF the API responds to a logout request with any HTTP status of 419 or any status of 500 or higher, OR the request fails due to a network error, OR no response is received within 10 seconds measured from the moment the request was dispatched, THEN THE Frontend SHALL retain the SPA auth state as "authenticated", SHALL re-enable the logout control, and SHALL display a non-blocking error message indicating that the logout request failed, the message remaining visible until dismissed by the User or until the User retries.
7. IF the User activates the logout control AND no CSRF_Cookie is present or the CSRF_Cookie value is empty, THEN THE Frontend SHALL NOT send a `POST /api/logout` request, SHALL retain the SPA auth state as "authenticated", and SHALL display a non-blocking error message indicating that the logout request could not be initiated.

### Requirement 11: Session Lifetime

**User Story:** As an operator, I want sessions to expire after a bounded period of inactivity, so that an unattended browser does not stay logged in indefinitely.

#### Acceptance Criteria

1. THE Backend SHALL configure the Auth_Session idle lifetime to 120 minutes, so that an Auth_Session whose last-recorded activity timestamp is more than 120 minutes earlier than the current server time is treated as expired by every endpoint of the API.
2. WHEN the Backend processes any API request that successfully resolves to an Authenticated_User via a non-expired Auth_Session, THE Backend SHALL update that Auth_Session's last-recorded activity timestamp to the current server time before returning the response, so that the 120-minute idle window defined in Requirement 11.1 is reset on each such request.
3. WHEN a request carries a Session_Cookie that references an Auth_Session that is expired per Requirement 11.1 or that does not exist server-side, THE Backend SHALL treat the request as anonymous for every endpoint of the API, SHALL NOT extend or re-activate the referenced Auth_Session, and SHALL set a response directive that instructs the user agent to delete the Session_Cookie.
4. THE Backend SHALL NOT issue any "remember me" cookie, persistent login token, or any other authentication artifact whose lifetime exceeds the 120-minute idle lifetime defined in Requirement 11.1 in response to any endpoint introduced by this feature, and SHALL NOT accept any client-supplied request parameter, header, or cookie that lengthens that idle lifetime.

### Requirement 12: Password Hygiene

**User Story:** As an operator, I want the System to handle passwords safely throughout the request lifecycle, so that a compromised log file or response cannot leak credentials.

#### Acceptance Criteria

1. THE Backend SHALL pass every plaintext password value supplied by a User on `POST /api/login` or by the Provisioning_Command through the configured Laravel password hashing algorithm before persisting the value, SHALL store only the resulting hash in the `password` column of the `users` table, and SHALL NOT persist any plaintext password value to any column, cache entry, queue payload, session entry, or filesystem location.
2. THE Backend SHALL NOT include any plaintext password value, password hash value, `remember_token` value, or other authentication secret in the body of any successful response, validation error response, authentication error response, or 5xx response emitted by the API.
3. THE Backend SHALL NOT write any plaintext password value supplied to `POST /api/login` or to the Provisioning_Command into any log entry produced via the log channels configured in `config/logging.php`, including request logs, error logs, debug logs, and exception traces.
4. IF the Backend processes a `POST /api/login` request that fails because of a Validation_Error, an authentication failure per Requirement 4.4, a rate-limit rejection per Requirement 4.7, or an unhandled exception, THEN THE Backend SHALL NOT include the supplied `password` value in the response body or in any log entry, and SHALL include only the supplied `email` value insofar as it appears in the response body or log entry produced for normal operation.
5. IF the configured Laravel password hashing algorithm raises an error while hashing a supplied password, THEN THE Backend SHALL fail the surrounding operation, SHALL NOT persist any User row whose `password` column would hold a non-hash value, SHALL NOT include the supplied plaintext password in the response body or any log entry, and SHALL surface a generic error message to the caller.

### Requirement 13: API Response Envelope and Error Handling

**User Story:** As a Frontend developer, I want every authentication failure to follow the existing API response envelope, so that the Frontend can render auth errors with the same code paths as other errors.

#### Acceptance Criteria

1. WHEN the Backend emits a response with HTTP status 401, 419, 422, or 429 from `POST /api/login`, `POST /api/logout`, `GET /api/user`, or any URL of the form `/api/contacts`, `/api/contacts/{id}`, or any sub-path of `/api/contacts/`, THE Backend SHALL set the `Content-Type` response header to `application/json; charset=utf-8` and SHALL emit a non-empty JSON response body.
2. THE Backend SHALL serialise every response with HTTP status 401, 419, 422, or 429 emitted by `POST /api/login`, `POST /api/logout`, `GET /api/user`, or any `/api/contacts` URL as a single JSON object whose top-level `message` field is a string of 1 to 500 characters after trimming leading and trailing whitespace, where the additional top-level `errors` field SHALL be present only on HTTP status 422 responses and SHALL conform to the Validation_Error shape defined in the Glossary, and where no top-level field other than `message` and (on 422) `errors` SHALL be present.
3. WHEN the Backend returns HTTP status 401 from `POST /api/login`, `POST /api/logout`, `GET /api/user`, or any `/api/contacts` URL, THE response body SHALL be a JSON object of the form `{ "message": <string> }` whose `message` is a string of 1 to 500 characters, and the response SHALL NOT cause an HTTP redirect and SHALL NOT include a `Location` response header.
4. WHEN the Backend returns HTTP status 419 from `POST /api/login`, `POST /api/logout`, `GET /api/user`, or any `/api/contacts` URL, THE response body SHALL be a JSON object of the form `{ "message": <string> }` whose `message` is a string of 1 to 500 characters, and the response SHALL NOT cause an HTTP redirect and SHALL NOT include a `Location` response header.
5. WHEN the Backend returns HTTP status 429 from `POST /api/login`, THE response body SHALL be a JSON object of the form `{ "message": <string> }` whose `message` is a string of 1 to 500 characters, and the response SHALL NOT cause an HTTP redirect and SHALL NOT include a `Location` response header.
6. THE Backend SHALL NOT include in the body of any HTTP status 401, 419, or 429 response emitted by `POST /api/login`, `POST /api/logout`, `GET /api/user`, or any `/api/contacts` URL any of the following: stack trace text, absolute or relative server filesystem paths, framework or PHP exception class names, framework or PHP version strings, the supplied request `password` value, the supplied request `email` value, or any User_Resource field.
