# WBMS Flutter App — Authentication Hand-off (READ FIRST)

**To the app agent building the Flutter app.** This supersedes any earlier
instruction that used a **tenant slug** (`X-Tenant`) or an **`api_key`**.

## ⛔ What to remove

The app currently asks the user for a **tenant slug** and an **API key**. Delete
that. It does not work against this backend:

- There are **no tenant records** — the `X-Tenant` lookup always fails with
  `404 Tenant not found`.
- The `api_key` was **never validated** by the server (any string passed). It is
  not a real credential. There is nothing to "get" or enter.

Remove the tenant-slug field, the api-key field, and any `X-Tenant` /
`api_key` headers or body params from the HTTP client.

## ✅ What to build instead: email + password login

The app authenticates each **user** (a person) with their WBMS account
(email + password). The server returns a bearer **token** and the user's
**role** and **company** — no tenant slug, no api key.

- **Base URL**: `https://billing.happyimart.com/api/v1`
- Auth scheme: `Authorization: Bearer <token>` (Laravel Sanctum)

### Where do credentials come from?
Users are created in the WBMS **web admin** (the `users` table). The app does not
register users. The person simply enters the **same email + password** they use
on the website. There is no tenant/company to pick — it comes back in the login
response.

---

## Endpoints (all live and verified)

### 1. Login
```
POST /api/v1/auth/login
Content-Type: application/json
Accept: application/json
```
**Body**
```json
{
  "email": "user@example.com",
  "password": "their-password",
  "device_name": "Pixel 8"
}
```
- `device_name` is **required** (any non-empty string identifying the device).
  Re-logging in with the same `device_name` revokes the old token for that device.

**200 OK**
```json
{
  "token": "12|abcdef...longstring",
  "user": {
    "id": 42,
    "name": "Maria Santos",
    "email": "user@example.com",
    "role": "meter_reader",
    "company_id": 3,
    "company": { "id": 3, "name": "Panganiban Water System", "slug": "panganiban-water-system-VWbvJqUSfn8y" }
  }
}
```
Store `token` securely and keep `user` for role-based UI.

**Errors**
| Status | Body | Meaning |
|--------|------|---------|
| 401 | `{ "message": "Invalid credentials", "errors": { "email": ["Invalid credentials"] } }` | Wrong email/password |
| 403 | `{ "message": "This account cannot sign in from the mobile app." }` | A super-admin account — not for the app |
| 403 | `{ "message": "Company account is disabled." }` | The user's company is deactivated |
| 422 | `{ "message": "...", "errors": { ... } }` | Missing/invalid email, password, or device_name |

### 2. Current user
```
GET /api/v1/auth/me
Authorization: Bearer <token>
Accept: application/json
```
**200** → `{ "user": { ...same shape as above... } }`
Use on app start to validate a stored token.

### 3. Logout
```
POST /api/v1/auth/logout
Authorization: Bearer <token>
```
**204 No Content** — revokes the current token. Then clear local storage.

### Token expiry / 401 handling
On **any** `401 { "message": "Unauthenticated." }`, the token is missing/expired/
revoked → clear stored token and show the login screen.

---

## Roles returned in `user.role`

The app shows different screens per role. Values:

| `role` value | Who |
|--------------|-----|
| `company_admin` | Full company access |
| `cashier` | Payments / collections |
| `meter_reader` | Field meter reading |
| `installer` | Install / disconnection work orders |
| `staff` | Read-only operational |
| `customer` | Self-service (own account) |

`super_admin` will never reach the app (blocked at login). Role checks in the app
are for **UX only** — the server enforces permissions and returns `403` for any
disallowed action.

---

## Flutter implementation (copy-paste ready)

### HTTP client — no tenant, no api key
```dart
// lib/core/api_client.dart
import 'package:dio/dio.dart';

const kBaseUrl = 'https://billing.happyimart.com/api/v1';

class ApiClient {
  ApiClient(this._tokenStore)
      : dio = Dio(BaseOptions(
          baseUrl: kBaseUrl,
          connectTimeout: const Duration(seconds: 15),
          receiveTimeout: const Duration(seconds: 20),
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
          },
        )) {
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        final token = _tokenStore.token;
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        handler.next(options); // NOTE: no X-Tenant, no api_key
      },
      onError: (e, handler) {
        if (e.response?.statusCode == 401) {
          _tokenStore.clear(); // -> redirect to login
        }
        handler.next(e);
      },
    ));
  }

  final Dio dio;
  final TokenStore _tokenStore;
}
```

### Auth service
```dart
class AuthService {
  AuthService(this._api, this._store);
  final ApiClient _api;
  final TokenStore _store;

  Future<AppUser> login(String email, String password, String deviceName) async {
    final res = await _api.dio.post('/auth/login', data: {
      'email': email,
      'password': password,
      'device_name': deviceName,
    });
    final token = res.data['token'] as String;
    final user = AppUser.fromJson(res.data['user']);
    await _store.save(token, user);   // secure storage
    return user;
  }

  Future<AppUser> me() async {
    final res = await _api.dio.get('/auth/me');
    return AppUser.fromJson(res.data['user']);
  }

  Future<void> logout() async {
    try { await _api.dio.post('/auth/logout'); } catch (_) {}
    await _store.clear();
  }
}

class AppUser {
  final int id;
  final String name, email, role;
  final int companyId;
  final String companyName;
  AppUser({required this.id, required this.name, required this.email,
           required this.role, required this.companyId, required this.companyName});

  factory AppUser.fromJson(Map<String, dynamic> j) => AppUser(
        id: j['id'], name: j['name'], email: j['email'], role: j['role'],
        companyId: j['company_id'],
        companyName: j['company']?['name'] ?? '',
      );
}
```

### Token storage — use secure storage
```dart
// Use flutter_secure_storage (Keychain/Keystore), NOT SharedPreferences.
final storage = const FlutterSecureStorage();
await storage.write(key: 'auth_token', value: token);
```

### Login screen contract
Two fields only: **Email** and **Password**. No tenant, no API key.
On submit → `AuthService.login(email, password, deviceName)`.
`deviceName` can be derived from `device_info_plus` (e.g. model name) or any
stable string.

---

## Quick test (so the agent can verify connectivity)

```bash
curl -X POST https://billing.happyimart.com/api/v1/auth/login \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"email":"<a real WBMS user email>","password":"<password>","device_name":"curl"}'
```
Expect `200` with a `token` and `user.role`. Then:
```bash
curl https://billing.happyimart.com/api/v1/auth/me \
  -H "Accept: application/json" -H "Authorization: Bearer <token>"
```

---

**Summary for the agent:** delete tenant slug + api key. Build an email/password
login that POSTs to `/api/v1/auth/login`, stores the returned bearer token, sends
it as `Authorization: Bearer`, and switches screens on `user.role`. That's the
entire auth contract.
</content>
