> ## Documentation Index
> Fetch the complete documentation index at: https://internal.mechzie.in/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Firebase OTP → JWT token exchange, refresh flow, and token management for MechZie

## Overview

MechZie uses **Firebase Phone Authentication** for identity verification and issues its own **JWT access + refresh token pair** for API authorization. The frontend never sends Firebase tokens after the initial exchange.

```mermaid theme={null}
sequenceDiagram
    participant App as Flutter App
    participant FB as Firebase Auth
    participant API as MechZie API

    App->>FB: Send OTP to phone
    FB-->>App: SMS code delivered
    App->>FB: Verify SMS code
    FB-->>App: Firebase ID Token
    App->>API: POST /auth/verify-otp
    API-->>App: { accessToken, refreshToken, user }
    App->>API: All subsequent requests with Bearer accessToken
```

## Auth Flow

<Steps>
  <Step title="Send OTP via Firebase">
    Use the Firebase Auth SDK in your Flutter app to send an OTP to the user's phone number. The MechZie API is **not involved** in this step.

    ```dart theme={null}
    await FirebaseAuth.instance.verifyPhoneNumber(
      phoneNumber: '+919876543210',
      verificationCompleted: (credential) async {
        await FirebaseAuth.instance.signInWithCredential(credential);
      },
      verificationFailed: (e) => handleError(e),
      codeSent: (verificationId, resendToken) {
        // Store verificationId for Step 2
      },
      codeAutoRetrievalTimeout: (verificationId) {},
    );
    ```
  </Step>

  <Step title="Verify OTP and get Firebase ID Token">
    After the user enters the OTP code, verify it and extract the Firebase ID token.

    ```dart theme={null}
    final credential = PhoneAuthProvider.credential(
      verificationId: verificationId,
      smsCode: userEnteredCode,
    );
    final userCredential = await FirebaseAuth.instance.signInWithCredential(credential);
    final firebaseIdToken = await userCredential.user!.getIdToken();
    ```
  </Step>

  <Step title="Exchange for MechZie tokens">
    Send the Firebase ID token to the MechZie API to get your JWT pair.

    ```bash theme={null}
    curl -X POST https://mechzie-api-production.run.app/api/v1/auth/verify-otp \
      -H "Content-Type: application/json" \
      -d '{
        "firebaseIdToken": "eyJhbGciOiJSUzI1NiIs...",
        "role": "customer"
      }'
    ```

    **Response (200):**

    ```json theme={null}
    {
      "accessToken": "eyJhbGciOiJIUzI1NiIs...",
      "refreshToken": "a1b2c3d4e5f6...",
      "user": {
        "id": "uuid-here",
        "phone": "+919876543210",
        "name": null,
        "email": null,
        "role": "customer",
        "avatar_url": null,
        "created_at": "2026-05-21T10:00:00.000Z"
      }
    }
    ```

    <Warning>
      The `role` field must be `customer` or `mechanic`. If the user already exists with a different role, the API returns **403 ROLE\_CONFLICT**. Users cannot switch roles.
    </Warning>
  </Step>

  <Step title="Store tokens securely">
    Store both tokens in secure storage (e.g. `flutter_secure_storage`). Never store in SharedPreferences or local storage.

    ```dart theme={null}
    await secureStorage.write(key: 'accessToken', value: response.accessToken);
    await secureStorage.write(key: 'refreshToken', value: response.refreshToken);
    ```
  </Step>
</Steps>

## Using the Access Token

Include the access token in all authenticated API requests:

```dart theme={null}
final response = await http.get(
  Uri.parse('$baseUrl/api/v1/users/me'),
  headers: {'Authorization': 'Bearer $accessToken'},
);
```

For Socket.io connections, pass it in the handshake:

```dart theme={null}
final socket = io('$baseUrl', OptionBuilder()
  .setTransports(['websocket'])
  .setAuth({'token': accessToken})
  .build()
);
```

## Token Refresh

Access tokens expire after **15 minutes**. Refresh tokens are valid for **30 days**. When the access token expires, use the refresh token to get a new pair:

```bash theme={null}
curl -X POST https://mechzie-api-production.run.app/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "a1b2c3d4e5f6..."
  }'
```

**Response (200):**

```json theme={null}
{
  "accessToken": "new-access-token...",
  "refreshToken": "new-refresh-token..."
}
```

<Warning>
  **Token rotation is enforced.** The old refresh token is revoked immediately. You must store the new refresh token from each response. Using a revoked refresh token returns **401** and requires full re-authentication via Firebase.
</Warning>

### Recommended: Interceptor Pattern

```dart theme={null}
class AuthInterceptor extends Interceptor {
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      try {
        final newTokens = await refreshTokens();
        // Retry the original request with new access token
        final retryResponse = await retry(err.requestOptions, newTokens.accessToken);
        return handler.resolve(retryResponse);
      } catch (e) {
        // Refresh failed — redirect to Firebase login
        await redirectToLogin();
      }
    }
    return handler.next(err);
  }
}
```

## Logout

Revoke a specific refresh token. The client should discard both tokens.

```bash theme={null}
curl -X POST https://mechzie-api-production.run.app/api/v1/auth/logout \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "a1b2c3d4e5f6..."
  }'
```

<Note>
  This only revokes the single refresh token sent. Other devices remain logged in. Suspension (by admin) revokes **all** refresh tokens for the user.
</Note>

## JWT Access Token Payload

The access token is an HS256 JWT with this payload:

```json theme={null}
{
  "sub": "user-uuid",
  "role": "customer",
  "firebaseUid": "firebase-uid-string",
  "type": "access",
  "iat": 1716300000,
  "exp": 1716300900
}
```

| Field         | Type   | Description                                       |
| ------------- | ------ | ------------------------------------------------- |
| `sub`         | UUID   | The user's ID in MechZie                          |
| `role`        | string | `customer`, `mechanic`, `admin`, or `super_admin` |
| `firebaseUid` | string | Firebase Auth UID                                 |
| `type`        | string | Always `access` for access tokens                 |
| `iat`         | number | Issued at (Unix timestamp)                        |
| `exp`         | number | Expires at (15 min after `iat`)                   |

## Admin Invite Flow

Admin accounts are **invite-only**. They cannot be created via `verify-otp`.

1. A `super_admin` creates an invite → `POST /api/v1/super-admin/invites` with `{ email, role }`
2. An invite link is sent to the email
3. The invitee signs up with Firebase (email must match) and calls:

```bash theme={null}
curl -X POST https://mechzie-api-production.run.app/api/v1/auth/complete-admin-invite \
  -H "Content-Type: application/json" \
  -d '{
    "firebaseIdToken": "eyJhbGci...",
    "inviteToken": "invite-token-from-email"
  }'
```

The role is always taken from the invite, never from the request body.

## User Roles

| Role          | How to get it                        | Capabilities                                               |
| ------------- | ------------------------------------ | ---------------------------------------------------------- |
| `customer`    | `verify-otp` with `role: "customer"` | Create jobs, manage vehicles, make payments                |
| `mechanic`    | `verify-otp` with `role: "mechanic"` | Accept jobs, update location, add line items               |
| `admin`       | Invite only                          | Dashboard, verify mechanics, manage jobs, refunds          |
| `super_admin` | Invite only                          | Everything admin can do + invite management + role changes |

<Warning>
  A user registered as `customer` **cannot** later authenticate as `mechanic` (or vice versa). The API returns **403 ROLE\_CONFLICT**. Each phone number is locked to one role.
</Warning>

## Error Responses

| Scenario                                 | HTTP | Error Code            |
| ---------------------------------------- | ---- | --------------------- |
| Invalid/expired Firebase token           | 401  | `UNAUTHORIZED`        |
| Invalid/expired refresh token            | 401  | `UNAUTHORIZED`        |
| Missing Authorization header             | 401  | `UNAUTHORIZED`        |
| User suspended by admin                  | 401  | `UNAUTHORIZED`        |
| Role mismatch (customer trying mechanic) | 403  | `ROLE_CONFLICT`       |
| Rate limited (too many OTP exchanges)    | 429  | `RATE_LIMIT_EXCEEDED` |
