# Customer User Accounts Plan

Date: 2026-04-27

## Goal

Add secure customer accounts for the public-facing Coconut AI apps, primarily portal app and secondarily dine-in app.

The account system should unlock:

- persistent online order and delivery/takeout management
- persistent booking management
- cross-app language preferences
- order history, favorites, spend tracking, VIP/perk eligibility
- surveys for frequent or VIP users
- user-submitted menu translation improvements
- reservation preordering by logged-in customers

This document started as the design and sequencing plan. It now also tracks the implementation status for the agreed rollout slices.

## Current State Summary

### API

The API is organized as endpoint files plus shared handler classes.

Relevant current entry points:

- `portal-app/order/place-new-order.php`
- `portal-app/order/get-order-data.php`
- `portal-app/order/change-order-scheduler.php`
- `portal-app/reservation/create-reservation.php`
- `portal-app/reservation/get-reservation-data.php`
- `portal-app/reservation/cancel-reservation.php`
- `dine-in-app/session/get-initial-page-load.php`
- `dine-in-app/session/place-new-order.php`
- `dine-in-app/session/set-customer-language.php`

Existing auth:

- `AppAuthHandler::verifyAppAuth(...)` validates app-level tokens.
- `AppAuthHandler::verifyUserAuth(...)` validates internal app users or dine-in session customers.
- Dine-in "user auth" currently means a table-session customer in `sessionCustomers`, not a permanent public account.
- Staff/admin login currently compares stored passwords directly in `lib/common/login-handler.php`, so public user auth should not copy that pattern.

Current portal customer flows are anonymous:

- online orders are looked up by order id
- reservations are controlled with reservation code + phone
- "last order", "last reservation", contact details, cart, and language are localStorage based

Current dine-in customer identity is session scoped:

- `sessionCustomers.authToken` identifies a customer inside a table session
- language is stored on `sessionCustomers.lang`
- localStorage stores dine-in session token, receipt code, cart, vendor URL, and language

### Frontend

Portal app currently stores customer state directly in localStorage:

- `lastVisitedOrderId`
- `lastVisitedOrderVendorUrl`
- `lastUsedPhoneNumber`
- `lastUsedName`
- `lastUsedUnit`
- `lastUsedAddress`
- `lastOrderType`
- `vendorReservations`
- `lastSelectedLanguageCode`
- `cart`
- `lastCartVendorUrl`

Portal also has an `AuthService`, but it is not a real public account auth implementation yet. It stores a token under `manager-app-user-auth-token`, which should be renamed/replaced during the account work.

Dine-in has a structured `LocalStorageService`, but its `userAuthToken` is the table-session token, not a permanent account token.

### Database

The existing schema already supports:

- `onlineOrders` and related item tables
- `reservations`
- `reservationPreorderItems`
- `sessionCustomers`
- menu translation tables
- cache state tables

The missing piece is a secure permanent customer identity layer and link columns from existing business records to that identity.

## Security Principles

Use a separate public customer auth domain. Do not reuse staff/manager/waiter/CEO auth tables, tokens, or login logic.

Follow these baseline rules:

- Passwords must use `password_hash()` and `password_verify()`, preferably `PASSWORD_ARGON2ID` where available.
- Customer passwords should stay low-friction: current first release policy is 6-128 characters with no composition rules. Compensate on the backend with strong hashing, generic auth errors, rate limiting, and security-event logging.
- Never store raw session tokens. Store only hashes of random tokens.
- Use high-entropy random bytes from `random_bytes()`.
- Prefer HttpOnly, Secure cookies for customer sessions instead of localStorage bearer tokens.
- If cookie auth is used, replace wildcard CORS for authenticated endpoints with explicit allowed origins and `Access-Control-Allow-Credentials: true`.
- Add CSRF protection for cookie-authenticated state-changing requests.
- Keep app auth and customer auth separate. App auth says "this request came from portal/dine-in app"; customer auth says "this is customer X".
- Public identifiers exposed to the frontend should be random public IDs, not sequential database IDs.
- Minimize PII. Store only what is needed for the feature, and plan account deletion/anonymization early.

Security references:

- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Password Storage Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

## Proposed Account Architecture

### Auth Model

Create a new customer auth system independent of current app auth.

Recommended session approach:

- Customer logs in from portal or dine-in.
- API creates a random session token.
- API stores `hash('sha256', token)` in `customerUserSessions`.
- API sets an HttpOnly, Secure cookie.
- Frontend never reads the session token.
- Frontend calls `/me` to know whether the user is logged in.
- Frontend includes a CSRF token header for state-changing requests.

Cookie details:

- Cookie name: `cai_customer_session`
- HttpOnly: yes
- Secure: yes
- SameSite: `Lax`
- Domain: prefer host-only for `api.coconut-ai.com` if possible. Use `.coconut-ai.com` only if necessary.
- Expiration: five-year rolling renewal, plus immediate server-side revocation on logout, password reset, account disable, or account deletion. This makes account login effectively persistent until the customer logs out, while still allowing server-side safety revocation.

Local development note:

- When the dev API receives a credentialed request from `localhost` or `127.0.0.1`, the customer session cookie uses `SameSite=None; Secure` so local Angular browser testing can persist login across reloads while calling `https://dev-api.coconut-ai.com`.
- Production keeps `SameSite=Lax`, which is appropriate for `coconut-ai.com` and `api.coconut-ai.com` because they are same-site subdomains.

CSRF:

- On login/session creation, return a CSRF token in the JSON response.
- Store only the CSRF token in memory or sessionStorage on the frontend.
- Store a hash of the CSRF token with the session row.
- Require `X-CSRF-Token` on state-changing authenticated endpoints.

### Login Options

Phase 1 should support:

- email + password
- email verification
- password reset
- logout from current device
- logout all devices

Do not start with social login. It adds operational complexity before the internal account model is stable.

Future optional login methods:

- email magic link
- passkeys/WebAuthn
- OAuth providers
- MFA for high-value accounts

## Proposed Database Upgrade

Use the same migration-doc style already present in `coconut-ai-api/docs`.

### Core Account Tables

`customerUsers`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `publicId` CHAR(36) NOT NULL UNIQUE
- `email` VARCHAR(255) NOT NULL UNIQUE
- `emailNormalized` VARCHAR(255) NOT NULL UNIQUE
- `emailVerifiedAt` DATETIME NULL
- `passwordHash` VARCHAR(255) NULL
- `displayName` VARCHAR(120) NULL
- `defaultName` VARCHAR(120) NULL
- `defaultPhone` VARCHAR(40) NULL
- `defaultLanguage` VARCHAR(8) NOT NULL DEFAULT 'en'
- `analyticsConsent` TINYINT(1) NOT NULL DEFAULT 0
- `marketingConsent` TINYINT(1) NOT NULL DEFAULT 0
- `status` ENUM('active','pending','disabled','deleted') NOT NULL DEFAULT 'active'
- `createdAt` DATETIME NOT NULL
- `updatedAt` DATETIME NOT NULL
- `lastLoginAt` DATETIME NULL

`customerUserSessions`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NOT NULL
- `sessionTokenHash` CHAR(64) NOT NULL UNIQUE
- `csrfTokenHash` CHAR(64) NOT NULL
- `createdAt` DATETIME NOT NULL
- `lastUsedAt` DATETIME NOT NULL
- `expiresAt` DATETIME NOT NULL
- `revokedAt` DATETIME NULL
- `ipHash` CHAR(64) NULL
- `userAgentHash` CHAR(64) NULL
- foreign key to `customerUsers(id)` ON DELETE CASCADE

`customerUserAuthChallenges`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NULL
- `emailNormalized` VARCHAR(255) NULL
- `type` ENUM('email_verification','password_reset','magic_login') NOT NULL
- `tokenHash` CHAR(64) NOT NULL UNIQUE
- `createdAt` DATETIME NOT NULL
- `expiresAt` DATETIME NOT NULL
- `usedAt` DATETIME NULL
- `requestIpHash` CHAR(64) NULL
- `userAgentHash` CHAR(64) NULL

`customerUserSecurityEvents`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NULL
- `eventType` VARCHAR(80) NOT NULL
- `createdAt` DATETIME NOT NULL
- `ipHash` CHAR(64) NULL
- `userAgentHash` CHAR(64) NULL
- `metadataJson` JSON NULL

### Preferences and Profile

`customerUserVendorProfiles`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NOT NULL
- `vendorId` VARCHAR(50) NOT NULL
- `displayName` VARCHAR(120) NULL
- `phone` VARCHAR(40) NULL
- `defaultUnit` VARCHAR(80) NULL
- `defaultAddress` VARCHAR(500) NULL
- `preferredLanguage` VARCHAR(8) NULL
- `vipTier` ENUM('none','regular','vip','super_vip') NOT NULL DEFAULT 'none'
- `bonusPoints` INT UNSIGNED NOT NULL DEFAULT 0
- `createdAt` DATETIME NOT NULL
- `updatedAt` DATETIME NOT NULL
- UNIQUE (`userId`, `vendorId`)

Use this table for vendor-specific defaults and loyalty/VIP state. Keep global settings on `customerUsers`.

### Linking Existing Records

Add nullable account links:

- `onlineOrders.customerUserId` BIGINT UNSIGNED NULL
- `reservations.customerUserId` BIGINT UNSIGNED NULL
- `sessionCustomers.customerUserId` BIGINT UNSIGNED NULL

Add indexes:

- `onlineOrders(customerUserId, placedTimestamp)`
- `reservations(customerUserId, dateYear, dateMonth, dateDay)`
- `sessionCustomers(customerUserId, sessionId)`

Keep all existing anonymous flows working. These columns should be nullable and backfilled opportunistically.

### Favorites and Interaction Tracking

`customerMenuItemEvents`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NULL
- `anonymousClientId` CHAR(36) NULL
- `vendorId` VARCHAR(50) NOT NULL
- `app` ENUM('portal','dine-in') NOT NULL
- `eventType` ENUM('view_item','add_to_cart','remove_from_cart','place_order','reorder','favorite','unfavorite') NOT NULL
- `menuItemId` INT UNSIGNED NULL
- `quantity` SMALLINT UNSIGNED NULL
- `metadataJson` JSON NULL
- `createdAt` DATETIME NOT NULL

`customerMenuItemStats`

- `userId` BIGINT UNSIGNED NOT NULL
- `vendorId` VARCHAR(50) NOT NULL
- `menuItemId` INT UNSIGNED NOT NULL
- `orderedQuantity` INT UNSIGNED NOT NULL DEFAULT 0
- `orderCount` INT UNSIGNED NOT NULL DEFAULT 0
- `lastOrderedAt` DATETIME NULL
- `favoriteScore` DECIMAL(10,3) NOT NULL DEFAULT 0
- PRIMARY KEY (`userId`, `vendorId`, `menuItemId`)

`customerFavoriteMenuItems`

- `userId` BIGINT UNSIGNED NOT NULL
- `vendorId` VARCHAR(50) NOT NULL
- `menuItemId` INT UNSIGNED NOT NULL
- `createdAt` DATETIME NOT NULL
- PRIMARY KEY (`userId`, `vendorId`, `menuItemId`)

Phase 1 can log only order-derived events. UI events should wait until analytics consent is clearly designed.

### Spend and VIP

`customerSpendSummaries`

- `userId` BIGINT UNSIGNED NOT NULL
- `vendorId` VARCHAR(50) NOT NULL
- `paidOrderCount` INT UNSIGNED NOT NULL DEFAULT 0
- `paidOrderTotal` DECIMAL(10,2) NOT NULL DEFAULT 0.00
- `paidDineInTotal` DECIMAL(10,2) NOT NULL DEFAULT 0.00
- `paidOnlineTotal` DECIMAL(10,2) NOT NULL DEFAULT 0.00
- `lastPaidOrderAt` DATETIME NULL
- `vipTier` ENUM('none','regular','vip','super_vip') NOT NULL DEFAULT 'none'
- `bonusPoints` INT UNSIGNED NOT NULL DEFAULT 0
- `updatedAt` DATETIME NOT NULL
- PRIMARY KEY (`userId`, `vendorId`)

VIP calculation should be server-side only. The frontend can display the result, but should not calculate eligibility.

### Surveys

`customerSurveys`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `vendorId` VARCHAR(50) NULL
- `targetTier` VARCHAR(40) NULL
- `title` VARCHAR(255) NOT NULL
- `status` ENUM('draft','active','paused','closed') NOT NULL DEFAULT 'draft'
- `createdAt` DATETIME NOT NULL

`customerSurveyResponses`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `surveyId` BIGINT UNSIGNED NOT NULL
- `userId` BIGINT UNSIGNED NOT NULL
- `vendorId` VARCHAR(50) NULL
- `responseJson` JSON NOT NULL
- `createdAt` DATETIME NOT NULL
- UNIQUE (`surveyId`, `userId`)

### Translation Suggestions

`menuTranslationSuggestions`

- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `userId` BIGINT UNSIGNED NULL
- `vendorId` VARCHAR(50) NOT NULL
- `entityType` ENUM('menu_item','menu_item_description','addon','modification','category','subcategory') NOT NULL
- `entityId` INT UNSIGNED NOT NULL
- `languageCode` VARCHAR(8) NOT NULL
- `currentTranslation` TEXT NULL
- `suggestedTranslation` TEXT NOT NULL
- `status` ENUM('pending_ai','accepted','rejected','needs_human_review') NOT NULL DEFAULT 'pending_ai'
- `aiScore` DECIMAL(5,2) NULL
- `aiReason` TEXT NULL
- `createdAt` DATETIME NOT NULL
- `reviewedAt` DATETIME NULL
- `appliedAt` DATETIME NULL

AI should never blindly apply low-confidence or suspicious submissions. Define strict thresholds and always log the decision.

### Reservation Preorder

The system already has `reservationPreorderItems`. For customer account support:

- link `reservations.customerUserId`
- allow account-authenticated customers to manage preorder items for their own reservation
- preserve waiter app activation flow
- block preorder edits after vendor-defined cutoff or after reservation has been activated

## Backend Implementation Plan

### Phase 0 - Foundation and Safety

1. Add DB migration for the core account tables and nullable link columns.
2. Add `lib/customer-app/customer-auth-handler.php`.
3. Add token generation, token hashing, cookie setting, cookie clearing, CSRF validation helpers.
4. Add tests for token creation, token validation, expired sessions, revoked sessions, missing CSRF, invalid CSRF.
5. Add explicit authenticated CORS handling for customer auth endpoints.

Do not modify existing order/reservation behavior yet.

### Phase 1 - Basic Account Endpoints

Add wrappers under portal first:

- `portal-app/customer/register.php`
- `portal-app/customer/login.php`
- `portal-app/customer/logout.php`
- `portal-app/customer/me.php`
- `portal-app/customer/request-password-reset.php`
- `portal-app/customer/reset-password.php`
- `portal-app/customer/verify-email.php`
- `portal-app/customer/update-profile.php`
- `portal-app/customer/update-language.php`

Then expose equivalent dine-in wrappers only where needed:

- `dine-in-app/customer/me.php`
- `dine-in-app/customer/login.php`
- `dine-in-app/customer/logout.php`
- `dine-in-app/customer/update-language.php`
- `dine-in-app/customer/link-current-session.php`

All wrappers should call shared customer auth handlers rather than duplicating auth code.

### Phase 2 - Link Portal Orders and Reservations

For logged-in users:

- `place-new-order.php` sets `onlineOrders.customerUserId`.
- `create-reservation.php` sets `reservations.customerUserId`.
- `get-order-data.php` can return richer details for the owner.
- `get-reservation-data.php` can use account ownership instead of code + phone.
- `cancel-reservation.php` can allow authenticated cancellation by ownership.

Anonymous compatibility:

- order-id lookup still works as today
- reservation code + phone still works as today
- localStorage fallback remains until the account dashboard is mature

Optional claim flow:

- allow logged-in users to claim an existing order if they know order id + phone
- allow logged-in users to claim a reservation if they know reservation code + phone
- log all claim attempts
- rate limit failed claim attempts

### Phase 3 - Portal Frontend Account Shell

Add portal UI in small pieces:

1. Account service with `me`, `login`, `register`, `logout`.
2. Header account icon.
3. Right-side account drawer with inline login/register.
4. Account dashboard tabs:
   - active orders
   - past orders
   - bookings
   - language/profile
5. Replace localStorage order/reservation display with account data when logged in.
6. Keep anonymous localStorage behavior for guests.

### Phase 4 - Dine-in Account Link

Dine-in should remain usable without accounts.

Logged-in account support should:

- keep table-session `sessionCustomers.authToken` as the session identity
- optionally link `sessionCustomers.customerUserId`
- sync selected language to account
- attribute dine-in paid orders to account after payment/finalization
- keep cart/session localStorage behavior unchanged for guests

### Phase 5 - Favorites and Spend Tracking

Start with server-derived signals:

- when a portal order is paid, update `customerMenuItemStats`
- when a dine-in receipt is paid/finalized and session customer is linked, update stats
- build favorites from `customerMenuItemStats`

Only after consent design:

- add UI interaction events like view/add/remove/favorite
- expose explicit opt-in/out

### Phase 6 - VIP and Perks

Add vendor-configurable thresholds later, not in the account foundation.

Initial server-side outputs:

- `vipTier`
- `bonusPoints`
- eligible perks

Potential future perk tables:

- `customerPerkRules`
- `customerPerkGrants`
- `customerBonusPointLedger`

Do not let frontend decide free orders, points, or addon eligibility.

### Phase 7 - Surveys

Eligibility should be calculated server-side:

- frequent user
- VIP user
- recent completed order
- not already answered

Surveys should be non-blocking and dismissible.

### Phase 8 - Translation Suggestions

Add a suggestion action in menu item detail UI:

- user selects item/field/language
- submits suggested translation
- backend records suggestion
- AI evaluates
- high-confidence accepted suggestions can update translation tables
- lower-confidence suggestions go to human review

Security controls:

- rate limit submissions per user/vendor/day
- sanitize all submitted text
- log AI decision
- keep original translation and accepted suggestion history

### Phase 9 - Reservation Preordering in Portal

Expose account-owned preorder management:

- list upcoming reservations
- open reservation preorder page
- add/edit/remove preorder items
- validate menu availability and cutoff rules
- persist to `reservationPreorderItems`

Do not let anonymous users modify preorder items unless a separate secure reservation-access flow is designed.

## Frontend Implementation Notes

### Portal App

Create:

- `CustomerAuthService`
- `CustomerAccountService`
- customer auth interceptor or explicit `withCredentials` API calls
- account model in global state
- account pages/components

Implemented account shell:

- `CustomerAuthService` calls `me`, `login`, `register`, `logout`, and `update-profile` with cookie credentials enabled.
- `AppComponent` initializes `/me` on startup so the header account state reflects an existing HttpOnly cookie session.
- `HeaderComponent` owns the first customer account UI: a right-side account drawer opened from the rightmost header icon.
- The drawer is intentionally task-adjacent and does not route the customer away from ordering or booking.
- Logged-out customers see the login/create-account form immediately inside the drawer.
- Logged-in customers see a centered avatar, display name/email, logout action, a working profile section, and future account sections for online orders, bookings, dine-ins, favorites, and language.
- The profile section lets customers update nickname, customer name for delivery/pickup, default phone, and default language. Order/reservation ownership views are deferred to Slice 3.
- The side-panel account identity uses nickname first, then customer name, then email when neither name has been set. Email is treated as a fixed account ID, not an editable profile field.

Dark drawer design notes:

- The drawer uses a distinct dark-mode visual language that still fits the portal palette: `#444` base, darker surface blocks, `#303030` borders, warm peach account highlights, and the existing muted brown brand accent.
- Width is 90vw on mobile, leaving a visible/tappable left edge to close the drawer.
- The drawer starts under the header with a small 1vw overlap to avoid the rounded-corner visual gap.
- When the drawer is open, the header's lower-right radius is flattened so the header and drawer meet cleanly.
- The panel has no dedicated X close icon. Customers close it by tapping the header account icon again or the visible left-edge close strip.
- Success messages are created only after the panel has rendered, pulse gently, stay readable for roughly 3.4 seconds, then disappear.

Replace direct localStorage usage gradually:

- keep guest cart in localStorage
- keep guest last order/reservation in localStorage
- when logged in, server account data becomes primary
- after login, offer to import local guest cart/contact/order/reservation data

Language behavior:

- anonymous: use `lastSelectedLanguageCode`
- logged in: use account `defaultLanguage`
- changing language updates account and local fallback

### Dine-in App

Do not collapse dine-in table-session auth into account auth.

Dine-in needs two identities:

- session customer identity: "who is this guest at this table right now?"
- account identity: "which permanent user is this?"

The account identity should be optional and layered on top.

## Testing Plan

### API Unit/Integration Tests

Add integration tests for:

- registration success
- duplicate email
- login success
- login wrong password
- logout revokes session
- `/me` unauthenticated
- `/me` authenticated
- session expiry
- CSRF required for state-changing authenticated requests
- profile update success, missing CSRF, unauthenticated update, and invalid profile fields
- linked order creation
- linked reservation creation
- anonymous order/reservation still working
- account claim flow success/failure/rate limit
- dine-in session link success/failure

### Frontend Tests/Manual Verification

Portal:

- guest order flow unchanged
- guest reservation flow unchanged
- register/login/logout
- profile update
- logged-in order appears in account dashboard
- logged-in reservation appears in dashboard
- language persists across reloads and devices after login

Dine-in:

- table QR load unchanged
- session rejoin unchanged
- account login/link does not break table session token
- language sync works

## Risk Register

High-risk areas:

- Cookie/CORS transition for API auth.
- Accidentally breaking anonymous portal order/reservation flows.
- Confusing dine-in session auth with account auth.
- Storing public auth tokens in localStorage.
- Over-collecting PII or tracking without consent.
- VIP/free-order logic being calculated client-side.
- Translation suggestions updating production translations too aggressively.

Mitigations:

- Keep account auth additive and optional.
- Build and test one capability at a time.
- Keep guest flows as the fallback path.
- Use nullable link columns first.
- Add feature flags where practical.
- Add logs/security events before enabling self-service account management broadly.

## Recommended First Implementation Slice

Start with the smallest secure foundation:

1. DB migration for `customerUsers`, `customerUserSessions`, `customerUserAuthChallenges`, `customerUserSecurityEvents`, and nullable link columns.
2. Shared customer auth handler.
3. Portal register/login/logout/me endpoints.
4. Tests for auth/session/CSRF.
5. Minimal portal login UI and account state.

Do not implement favorites, VIP, surveys, translation suggestions, or preordering until this slice is stable in production.

## Agreed Product Decisions

- First login method: email + password only.
- Email delivery: use Coconut AI's own InMotion dedicated VPS / WHM server capability first.
- Account uniqueness: global by email across all vendors.
- Signup fields: email + password only.
- Password UX: 6-character minimum, no forced uppercase/symbol/number rules. Backend defenses carry the extra safety burden.
- Session lifetime: keep users logged in as long as practical for low-friction customer use. Use five-year rolling sessions with server-side revocation and renewal on return visits rather than localStorage tokens.
- Analytics/interaction consent: no explicit consent gate for the first release. Keep the implementation first-party, minimal, and focused on service/product improvement for the two Malaysia stores.
- Account deletion: include deletion/anonymization in the first backend design, even if the UI arrives later.
- First portal UI entry point: a subtle user icon at the far right of the header opening a dark right-side drawer.

## Next Concrete Implementation Plan

### Slice 1 - Secure Account Foundation

Deliverables:

1. Create SQL migration doc for:
   - `customerUsers`
   - `customerUserSessions`
   - `customerUserAuthChallenges`
   - `customerUserSecurityEvents`
   - nullable `customerUserId` links on `onlineOrders`, `reservations`, and `sessionCustomers`
2. Add shared backend auth code:
   - secure random token generation
   - token hashing
   - password hashing/verifying
   - HttpOnly Secure cookie setting/clearing
   - session lookup and renewal
   - CSRF token generation/validation
   - security event logging
3. Add portal customer endpoints:
   - `register.php`
   - `login.php`
   - `logout.php`
   - `me.php`
4. Add integration tests for:
   - register success
   - duplicate email
   - login success
   - wrong password
   - session cookie auth
   - logout revocation
   - unauthenticated `/me`
   - authenticated `/me`
   - CSRF required for authenticated state-changing requests

No portal UI changes in this slice except if needed for endpoint testing. This keeps the first change backend-only and much easier to validate safely.

Status: implemented.

Files:

- `docs/step-37-add-customer-user-accounts.sql`
- `docs/step-37-verify-customer-user-accounts.sql`
- `lib/customer-app/customer-auth-handler.php`
- `portal-app/customer/register.php`
- `portal-app/customer/login.php`
- `portal-app/customer/logout.php`
- `portal-app/customer/me.php`
- `tests/Integration/PortalApp/CustomerAuthEndpointTest.php`

Verified with:

- `phpunit tests/Integration/PortalApp/CustomerAuthEndpointTest.php`
- `phpunit tests/Integration/PortalApp`

### Slice 2 - Minimal Portal Account UI

Deliverables:

1. Add `CustomerAuthService`.
2. Add account state to portal global model.
3. Add minimal login/register/logout UI.
4. Add startup `/me` check.
5. Keep guest order/reservation flows unchanged.

Status: implemented.

Files:

- `coconut-ai-portal/src/app/services/customer-auth.service.ts`
- `coconut-ai-portal/src/app/app.component.ts`
- `coconut-ai-portal/src/app/header/header.component.ts`
- `coconut-ai-portal/src/app/header/header.component.html`
- `coconut-ai-portal/src/app/header/header.component.css`

Implemented behavior:

- The user icon is the final header action, closest to the right edge.
- Opening the icon toggles a 90vw right-side account drawer.
- The drawer starts just under the header with a 1vw overlap and keeps 10vw of the page visible for closing.
- Logged-out state shows inline login/create account controls without an extra navigation step.
- Logged-in state shows centered avatar/account identity plus future account sections.
- The section label was removed because the rows are visually self-evident.
- The profile row opens an in-panel profile view with a back button, fixed account ID/email, editable nickname, editable customer name, editable phone, and default language selector.
- Account-created and signed-in messages are centered, gently highlighted, delayed until after render, and auto-dismissed.
- Button borders were adjusted to the darker `#303030` family to avoid a distracting light-edge effect.
- Guest order and reservation flows are unchanged.

Verified with:

- `npm run build`

### Slice 2A - Portal Profile Preferences

Deliverables:

1. Add `portal-app/customer/update-profile.php`.
2. Add authenticated profile update handling to `CustomerAuthHandler`.
3. Allow updating only low-risk profile fields:
   - `displayName`
   - `defaultName`
   - `defaultPhone`
   - `defaultLanguage`
4. Require active customer session and valid CSRF token.
5. Add profile view inside the dark account drawer.
6. Keep email as the fixed account ID. Do not add email-change infrastructure in this slice.

Status: implemented.

Files:

- `coconut-ai-api/lib/customer-app/customer-auth-handler.php`
- `coconut-ai-api/docs/step-38-add-customer-default-name.sql`
- `coconut-ai-api/docs/step-38-verify-customer-default-name.sql`
- `coconut-ai-api/portal-app/customer/update-profile.php`
- `coconut-ai-api/tests/Integration/PortalApp/CustomerAuthEndpointTest.php`
- `coconut-ai-portal/src/app/services/customer-auth.service.ts`
- `coconut-ai-portal/src/app/header/header.component.ts`
- `coconut-ai-portal/src/app/header/header.component.html`
- `coconut-ai-portal/src/app/header/header.component.css`

Verified with:

- `phpunit tests/Integration/PortalApp/CustomerAuthEndpointTest.php`
- `npm run build`

### Slice 3 - Link Portal Orders and Reservations

Deliverables:

1. Attach `customerUserId` to new online orders when logged in.
2. Attach `customerUserId` to new reservations when logged in.
3. Add account dashboard endpoints for active orders and upcoming reservations.
4. Add portal dashboard UI.
5. Add optional guest-to-account import/claim flow.

## Remaining Decisions

- Exact outgoing email implementation on InMotion/WHM. Recommendation: create a small mail abstraction first so the backend can use local SMTP/sendmail now and switch provider later if deliverability becomes a problem.
