# Employee WhatsApp Login — Phase 2 Design

**Goal:** Complete the employee first-login flow by triggering OTP for unverified employees, forcing a password change after OTP verification, and blocking dashboard access until the password is changed.

**Depends on:** `docs/superpowers/specs/2026-07-03-employee-whatsapp-login.md` (Phase 1 — employee create/update by WhatsApp, unique index, login by `whatsapp_number`).

---

## Current State (after Phase 1)

- `users` table has `whatsapp_number` (nullable, unique) and `is_verified` (boolean).
- `POST /api/v1/login` accepts `email` or `whatsapp_number` + `password`.
- `AuthController::login` returns a generic `401 Invalid credentials.` when the user is unverified, even if the password is correct.
- `EmployeeController::store` creates employees with `is_verified => false`.
- `OtpService::send`, `verifyAccount`, and `resendOtp` already exist and work for owner/customer registration.
- There is no `must_change_password` column or forced password-change endpoint.

## Proposed Changes

### 1. Database — add `must_change_password` flag

Create a new migration:

```php
Schema::table('users', function (Blueprint $table) {
    $table->boolean('must_change_password')->default(false);
});
```

Add the cast to `User`:

```php
protected function casts(): array
{
    return [
        'password' => 'hashed',
        'is_verified' => 'boolean',
        'must_change_password' => 'boolean',
    ];
}
```

### 2. Employee creation sets the flag

In `EmployeeController::store`, when creating the user:

```php
$user = User::create([
    'name' => $data['name'],
    'email' => $data['email'] ?? null,
    'whatsapp_number' => $data['whatsapp_number'] ?? null,
    'password' => Hash::make($data['password']),
    'is_verified' => false,
    'must_change_password' => true,
]);
```

### 3. Unverified employee login triggers OTP

Change `AuthController::login` so that a correct `whatsapp_number` + password but `!is_verified` does **not** return `401 Invalid credentials.`. Instead:

- Look up the user by `email` or `whatsapp_number` as today.
- If no user or wrong password → `401 Invalid credentials.` (unchanged).
- If the password matches, the identifier is `whatsapp_number`, the user has the `employee` role, and `!is_verified`:
  - Send OTP via `OtpService::send($user->whatsapp_number)`.
  - Return `403` with a distinct error code and the phone number:

```json
{
  "message": "Account not verified.",
  "code": "account_unverified",
  "whatsapp_number": "+12025552000"
}
```

- In `local`/`testing`, include the generated OTP in the response body so tests can verify without a real WhatsApp gateway:

```json
{
  "message": "Account not verified.",
  "code": "account_unverified",
  "whatsapp_number": "+12025552000",
  "otp": "123456"
}
```

**Unchanged behavior:**
- Unknown user or wrong password → `401 Invalid credentials.`
- Unverified user logging in by `email` → `401 Invalid credentials.`
- Unverified non-employee (owner/customer) logging in by `whatsapp_number` → `401 Invalid credentials.`

This keeps the existing customer/owner registration flows untouched while giving the owner web app a detectable signal for newly created employees.

### 4. Expose `must_change_password` in auth responses

Update `LoginResource` to include the flag:

```php
return [
    'user' => UserResource::make($user),
    'permissions' => $user->getPermissionNames()->values(),
    'token' => $this->token,
    'logo_url' => ...,
    'must_change_password' => $user->must_change_password,
];
```

Also include it in `UserResource` so `GET /api/v1/profile` and other user payloads carry the flag:

```php
return [
    'id' => $this->id,
    'name' => $this->name,
    'email' => $this->email,
    'whatsapp_number' => $this->whatsapp_number,
    'must_change_password' => $this->must_change_password,
    'roles' => ...,
    'created_at' => ...,
];
```

### 5. Force-change-password endpoint

Add `POST /api/v1/auth/force-change-password` (or reuse `/api/v1/update-password` with conditional logic). The chosen design is a dedicated endpoint to avoid complicating the existing `updatePassword` rules.

Route:

```php
Route::post('/auth/force-change-password', [AuthController::class, 'forceChangePassword'])
    ->name('auth.force-change-password')
    ->middleware(['auth:api']);
```

Request rules:

```php
'password' => ['required', 'string', 'confirmed', Password::defaults()],
```

Controller behavior:

```php
public function forceChangePassword(Request $request): JsonResponse
{
    $request->validate([
        'password' => ['required', 'string', 'confirmed', Password::defaults()],
    ]);

    $user = auth()->user();

    $user->update([
        'password' => Hash::make($request->password),
        'must_change_password' => false,
    ]);

    $user->tokens()->delete();

    return AuthService::issueAuthToken($user)->response();
}
```

On success, the endpoint returns a fresh login resource (new token + `must_change_password: false`).

### 6. Post-OTP verification returns the flag

`AuthController::verifyAccount` already issues a token via `AuthService::issueAuthToken($user)`. Because `LoginResource` now includes `must_change_password`, employees who verify will receive `must_change_password: true` in the response and the web can redirect to the forced change-password screen.

No extra change is required in `verifyAccount` beyond the resource update.

### 7. Middleware to block dashboard access until password is changed

Create `EnsurePasswordChanged` middleware:

```php
class EnsurePasswordChanged
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();

        if ($user && $user->must_change_password) {
            return response()->json([
                'message' => 'Password change required.',
                'code' => 'password_change_required',
            ], 403);
        }

        return $next($request);
    }
}
```

Register it in `bootstrap/app.php` (or wherever middleware aliases are defined) as `password_changed`.

Apply it to the verified auth group **after** the existing `verified` middleware, but exclude the force-change-password route and logout route:

```php
Route::middleware(['auth:api', 'verified', 'password_changed'])->group(function () {
    Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->withoutMiddleware(['password_changed']);
    Route::post('/auth/force-change-password', [AuthController::class, 'forceChangePassword'])
        ->name('auth.force-change-password')
        ->withoutMiddleware(['password_changed']);

    // ... all other protected routes
});
```

Because the middleware runs on every authenticated route, an employee cannot bypass the forced change-password screen even if the frontend guard fails.

### 8. Existing `/api/v1/update-password`

No change required. It still requires `current_password`. Employees with `must_change_password === false` can use it normally.

## Data Flow (end-to-end)

1. Owner creates employee with `whatsapp_number` + initial password.
   - `is_verified = false`, `must_change_password = true`.
2. Employee logs in with `whatsapp_number` + temp password.
   - API validates password, sees `!is_verified`, sends OTP, returns `403 account_unverified`.
   - Web redirects to `/auth/verify?phone=...`.
3. Employee enters OTP on `/api/v1/verify-account`.
   - API sets `is_verified = true`, issues token.
   - Response includes `must_change_password: true`.
   - Web redirects to forced change-password screen.
4. Employee submits new password to `/api/v1/auth/force-change-password`.
   - API updates password, sets `must_change_password = false`, revokes old tokens, issues new token.
   - Web redirects to `/owner/dashboard`.
5. Later logins use `whatsapp_number` + new password and return a normal `200` login response.

## Error Handling

| Scenario | Response |
|----------|----------|
| Unknown `whatsapp_number` or wrong password | `401 { message: "Invalid credentials." }` |
| Correct password but `!is_verified` (WhatsApp login) | `403 { message: "Account not verified.", code: "account_unverified", whatsapp_number: "..." }` + OTP sent |
| Invalid/expired OTP | `401 { message: "Invalid or expired OTP" }` |
| Verified but `must_change_password` and hitting protected route | `403 { message: "Password change required.", code: "password_change_required" }` |
| Force-change-password with weak/unconfirmed password | `422` validation errors |

In `local`/`testing`, the OTP is included in the unverified-login response for testability.

## Testing

Add or update tests in `tests/Feature/Auth/LoginTest.php`, `tests/Feature/Auth/VerificationTest.php`, and `tests/Feature/Owner/EmployeeStoreTest.php`:

1. Create employee with WhatsApp only (no email) → `is_verified=false`, `must_change_password=true`.
2. Login with correct temp password + unverified → OTP sent, `403 account_unverified` (not `401`).
3. `POST /verify-account` with OTP → token + `must_change_password=true`.
4. Force-change password → flag cleared, normal login works afterward.
5. Login with `whatsapp_number` (verified, password changed) → `200` + token.
6. Duplicate `whatsapp_number` on create/update → `422`.
7. Owner updates employee `whatsapp_number` → unique-ignore-self works.
8. Middleware blocks protected routes when `must_change_password=true` and allows access after password change.

## Backwards Compatibility

- Existing customers and owners are created with `must_change_password` defaulting to `false`.
- The new `403 account_unverified` response only changes behavior for unverified users logging in with `whatsapp_number`; email logins stay unchanged.
- Existing `/api/v1/update-password` behavior is unchanged.
