> For the complete documentation index, see [llms.txt](https://lmile.gitbook.io/lmile-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lmile.gitbook.io/lmile-docs/operator-api/login.md).

# Operator Login — サインアップ・ログイン・トークン更新・アカウント削除

倉庫スタッフ向けピッキングアプリ（picking-ios）の認証エンドポイントです。スタッフはメールアドレス + パスワード、Google サインイン（Google ID token）、Sign in with Apple（identity token）、またはメールに届く確認コードで**ログイン**して operator JWT を発行します。アカウントはプラットフォーム / 管理者側で発行し、アプリ内のセルフサインアップはありません。有効期限が切れたら refresh token で更新します。`/api/operator/me` はログイン中スタッフの id / email / 束縛 shop 群を返します。`/api/operator/account/delete` は本人が自分のアカウントを即時削除します。発行された JWT は他の [Operator API](/lmile-docs/operator-api/lookup.md)（`/api/operator/*`）の `Authorization: Bearer` で使います。

> **Base URL** `https://api.lmile.io` · **認証** `login` / `login/google` / `login/apple` / `login/otp/*` / `refresh` は不要（`email`+`password` / `idToken` / `identityToken` / 確認コード / `refreshToken` で認証）、`me` / `account/delete` は operator JWT（`Authorization: Bearer`）（[詳細](/lmile-docs/getting-started/authentication.md)）

## エンドポイント

| Method | Path                              | 用途                                                                            |
| ------ | --------------------------------- | ----------------------------------------------------------------------------- |
| `POST` | `/api/operator/login`             | `email` + `password` → JWT / refreshToken / staff / shops を発行                 |
| `POST` | `/api/operator/login/google`      | Google ID token（`idToken`）→ JWT / refreshToken / staff / shops を発行            |
| `POST` | `/api/operator/login/apple`       | Apple identity token（`identityToken`）→ JWT / refreshToken / staff / shops を発行 |
| `POST` | `/api/operator/login/otp/request` | `email` → 確認コードをメールで送る                                                        |
| `POST` | `/api/operator/login/otp/verify`  | `email` + `code`（確認コード）→ JWT / refreshToken / staff / shops を発行               |
| `POST` | `/api/operator/refresh`           | `refreshToken` → 新 JWT / refreshToken を発行                                     |
| `GET`  | `/api/operator/me`                | ログイン中スタッフの id / email / 束縛 shop 群                                             |
| `POST` | `/api/operator/account/delete`    | 本人アカウントを即時削除（退会）。セッションも失効                                                     |

> レスポンスは camelCase（routing 系と同じ）。`/api/operator/*` の認証系統（operator JWT・staff↔shop 束縛）は [認証](https://lmile.gitbook.io/lmile-docs/operator-api/pages/7cB70I8yeRnMpvcdLkNu#operator-jwt-認証) を参照してください。

***

## `POST` /api/operator/login

スタッフのメールアドレスとパスワードを検証し、operator JWT・refresh token・スタッフ情報・束縛 shop 群を返します。束縛 shop が 0 件でも `ok: true` で返し（アプリ側が「shop 未割当」を案内できるように）、`shops: []` になります。

### リクエスト

```http
POST https://api.lmile.io/api/operator/login
Content-Type: application/json
```

```json
{
  "email": "staff@example.com",
  "password": "********"
}
```

### ボディパラメータ

| パラメータ      | 型      |  必須 | 説明                    |
| ---------- | ------ | :-: | --------------------- |
| `email`    | string |  ✓  | スタッフのメールアドレス（前後空白は無視） |
| `password` | string |  ✓  | パスワード                 |

### レスポンス

```json
{
  "ok": true,
  "jwt": "eyJhbGciOi...",
  "refreshToken": "v1.Mr8...",
  "expiresAt": 1749290400,
  "expiresIn": 3600,
  "tokenType": "bearer",
  "staff": { "id": "f1e2d3c4-b5a6-7890-1234-567890abcdef", "email": "staff@example.com" },
  "shops": ["demo-store.myshopify.com"],
  "shopNames": { "demo-store.myshopify.com": "Demo Store" }
}
```

| フィールド          | 説明                                                                                                                                                   |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jwt`          | 以降の `/api/operator/*` で `Authorization: Bearer` に使うアクセストークン                                                                                          |
| `refreshToken` | `expiresAt` 到来後に `/api/operator/refresh` へ渡す更新トークン                                                                                                   |
| `expiresAt`    | JWT の有効期限（Unix 秒。取得できない場合 `null`）                                                                                                                    |
| `expiresIn`    | 発行から失効までの秒数                                                                                                                                          |
| `tokenType`    | トークン種別（`bearer`）                                                                                                                                     |
| `staff`        | ログインしたスタッフの `id`（= ユーザー ID）/ `email`                                                                                                                 |
| `shops`        | スタッフに束縛された shop ドメイン群（0 件は `[]`）                                                                                                                     |
| `shopNames`    | shop ドメイン → 表示名（ブランド名）の map。表示名が登録されている shop のみ含み、未登録の shop はキーに現れません（アプリはその場合 shop ドメインをそのまま表示します）。アプリの shop / 拠点選択・設定画面でドメインの代わりにブランド名を表示するために使います |

### 挙動

* 認証失敗（メール不存在・パスワード不一致など）は、メールアドレスの存在有無を漏らさないため一律 `401 invalid_credentials` です。
* 試行回数の上限に達すると `429 rate_limited` を返します。
* 認証基盤に到達できない場合は `503 auth_unavailable`（旧経路にフォールバックしません）。

***

## `POST` /api/operator/login/google

Google サインインで取得した Google ID token を検証し、operator JWT・refresh token・スタッフ情報・束縛 shop 群を返します（レスポンスは `login` と同形）。アプリ側は expo-auth-session 等で Google ID token を取得してこのエンドポイントに渡します。staff↔shop 束縛はスタッフのユーザー単位なので、同じメールアドレスのスタッフは email + password ログインと同じ束縛 shop 群に解決されます。束縛 shop が 0 件でも `ok: true` + `shops: []` で返します。

### リクエスト

```http
POST https://api.lmile.io/api/operator/login/google
Content-Type: application/json
```

```json
{
  "idToken": "eyJhbGciOiJSUzI1NiIs..."
}
```

### ボディパラメータ

| パラメータ     | 型      |  必須 | 説明                                         |
| --------- | ------ | :-: | ------------------------------------------ |
| `idToken` | string |  ✓  | Google サインインで取得した Google ID token（前後空白は無視） |

### レスポンス

```json
{
  "ok": true,
  "jwt": "eyJhbGciOi...",
  "refreshToken": "v1.Mr8...",
  "expiresAt": 1749290400,
  "expiresIn": 3600,
  "tokenType": "bearer",
  "staff": { "id": "f1e2d3c4-b5a6-7890-1234-567890abcdef", "email": "staff@example.com" },
  "shops": ["demo-store.myshopify.com"],
  "shopNames": { "demo-store.myshopify.com": "Demo Store" }
}
```

フィールドの意味は [`POST /api/operator/login`](#post-apioperatorlogin) と同じです。

### 挙動

* ID token の検証失敗（不正・失効・別 provider の token など）は、原因の詳細を漏らさないため一律 `401 invalid_id_token` です。
* 試行回数の上限に達すると `429 rate_limited` を返します。
* 認証基盤に到達できない場合は `503 auth_unavailable`（旧経路にフォールバックしません）。

***

## `POST` /api/operator/login/apple

Sign in with Apple で取得した Apple identity token を検証し、operator JWT・refresh token・スタッフ情報・束縛 shop 群を返します（レスポンスは `login` と同形）。アプリ側は expo-apple-authentication で identity token を取得してこのエンドポイントに渡します。Google サインイン併設に対する Apple ガイドライン 4.8（同等の代替ログイン）対応です。staff↔shop 束縛はスタッフのユーザー単位で、束縛が 0 件（自己登録直後や Hide My Email の新規ユーザー）でも `ok: true` + `shops: []` で返し、アプリのオンボーディングで shop に紐付けます。

### リクエスト

```http
POST https://api.lmile.io/api/operator/login/apple
Content-Type: application/json
```

```json
{
  "identityToken": "eyJhbGciOiJSUzI1NiIs...",
  "authorizationCode": "c1a2b3...",
  "email": "staff@example.com",
  "fullName": "Hanako Yamada",
  "user": "001234.abcd...ef"
}
```

### ボディパラメータ

| パラメータ               | 型              |  必須 | 説明                                                                        |
| ------------------- | -------------- | :-: | ------------------------------------------------------------------------- |
| `identityToken`     | string         |  ✓  | Sign in with Apple で取得した Apple identity token（前後空白は無視）。認証はこの token で完結します |
| `authorizationCode` | string \| null |     | Apple の authorization code（初回のみ。本エンドポイントの認証には未使用）                         |
| `email`             | string \| null |     | Apple が返すメール（初回サインイン時のみ。Hide My Email の場合は relay メール）                     |
| `fullName`          | string \| null |     | Apple が返す氏名（初回サインイン時のみ）                                                   |
| `user`              | string         |     | Apple の安定ユーザー ID（`sub`。初回のみ届く付随情報）                                        |

### レスポンス

```json
{
  "ok": true,
  "jwt": "eyJhbGciOi...",
  "refreshToken": "v1.Mr8...",
  "expiresAt": 1749290400,
  "expiresIn": 3600,
  "tokenType": "bearer",
  "staff": { "id": "f1e2d3c4-b5a6-7890-1234-567890abcdef", "email": "staff@example.com" },
  "shops": [],
  "shopNames": {}
}
```

フィールドの意味は [`POST /api/operator/login`](#post-apioperatorlogin) と同じです。

### 挙動

* 認証は `identityToken` のみで完結します（`email` / `fullName` / `authorizationCode` / `user` は初回のみ届く付随情報で、検証には使いません）。
* identity token の検証失敗（不正・失効・別 provider の token・宛先 bundle ID 不一致など）は、原因の詳細を漏らさないため一律 `401 invalid_id_token` です。
* 試行回数の上限に達すると `429 rate_limited`、認証基盤に到達できない場合は `503 auth_unavailable`（フォールバックしません）。

***

## `POST` /api/operator/login/otp/request

スタッフのメールアドレスへ、ログイン用の確認コードを送ります。パスワードも Google / Apple のアカウントも使わないスタッフ向けのログイン方法で、届いたコードを [`/api/operator/login/otp/verify`](#post-apioperatorloginotpverify) に渡すとログインできます。

### リクエスト

```http
POST https://api.lmile.io/api/operator/login/otp/request
Content-Type: application/json
```

```json
{
  "email": "staff@example.com"
}
```

### ボディパラメータ

| パラメータ   | 型      |  必須 | 説明                           |
| ------- | ------ | :-: | ---------------------------- |
| `email` | string |  ✓  | スタッフのメールアドレス（前後空白と大文字小文字は無視） |

### レスポンス

```json
{
  "ok": true
}
```

### 挙動

* コードを送るのは、すでにアカウントがあるメールアドレスだけです。このエンドポイントでアカウントは作られません。
* アカウントが無いメールアドレスでも `ok: true` を返します（メールアドレスの存在有無を漏らさないため）。その場合メールは届きません。届かないときは、管理者にアカウントの有無を確認してください。
* 短い間隔で繰り返すと `429 rate_limited` を返します。少し待ってから送り直してください。
* 認証基盤に到達できない場合は `503 auth_unavailable`（フォールバックしません）。

***

## `POST` /api/operator/login/otp/verify

メールに届いた確認コードを照合し、operator JWT・refresh token・スタッフ情報・束縛 shop 群を返します（レスポンスは `login` と同形）。束縛 shop が 0 件でも `ok: true` + `shops: []` で返します。

### リクエスト

```http
POST https://api.lmile.io/api/operator/login/otp/verify
Content-Type: application/json
```

```json
{
  "email": "staff@example.com",
  "code": "12345678"
}
```

### ボディパラメータ

| パラメータ   | 型      |  必須 | 説明                    |
| ------- | ------ | :-: | --------------------- |
| `email` | string |  ✓  | 確認コードを送ったメールアドレス      |
| `code`  | string |  ✓  | メールに届いた確認コード（前後空白は無視） |

### レスポンス

```json
{
  "ok": true,
  "jwt": "eyJhbGciOi...",
  "refreshToken": "v1.Mr8...",
  "expiresAt": 1749290400,
  "expiresIn": 3600,
  "tokenType": "bearer",
  "staff": { "id": "f1e2d3c4-b5a6-7890-1234-567890abcdef", "email": "staff@example.com" },
  "shops": ["demo-store.myshopify.com"],
  "shopNames": { "demo-store.myshopify.com": "Demo Store" }
}
```

フィールドの意味は [`POST /api/operator/login`](#post-apioperatorlogin) と同じです。

### 挙動

* コードの誤り・期限切れなどは、原因の詳細を漏らさないため一律 `401 invalid_code` です。`/api/operator/login/otp/request` でコードを送り直してください。
* 試行回数の上限に達すると `429 rate_limited`、認証基盤に到達できない場合は `503 auth_unavailable`（フォールバックしません）。

***

## `POST` /api/operator/refresh

refresh token から新しい operator JWT を発行します（再ログイン不要のセッション継続用）。

### リクエスト

```http
POST https://api.lmile.io/api/operator/refresh
Content-Type: application/json
```

```json
{
  "refreshToken": "v1.Mr8..."
}
```

### ボディパラメータ

| パラメータ          | 型      |  必須 | 説明                                            |
| -------------- | ------ | :-: | --------------------------------------------- |
| `refreshToken` | string |  ✓  | `login` / 前回の `refresh` で受け取った更新トークン（前後空白は無視） |

### レスポンス

```json
{
  "ok": true,
  "jwt": "eyJhbGciOi...",
  "refreshToken": "v1.Nz2...",
  "expiresAt": 1749294000,
  "expiresIn": 3600,
  "tokenType": "bearer"
}
```

| フィールド                                   | 説明                               |
| --------------------------------------- | -------------------------------- |
| `jwt` / `refreshToken`                  | 新しいアクセストークン / 更新トークン（更新後はこちらを保持） |
| `expiresAt` / `expiresIn` / `tokenType` | `login` と同じ意味                    |

### 挙動

* 失効済み・不正な refresh token は `401 invalid_refresh_token`。
* 試行回数の上限は `429 rate_limited`、認証基盤に到達できない場合は `503 auth_unavailable`。

***

## `GET` /api/operator/me

operator JWT を検証し、ログイン中スタッフの id / email / 束縛 shop 群を返します。ログイン直後の shop 選択画面が使います。束縛 shop が 0 件でも `200` + `shops: []` を返します（「shop 未割当」を案内できるように）。

### リクエスト

```http
GET https://api.lmile.io/api/operator/me
Authorization: Bearer {OPERATOR_JWT}
```

### レスポンス

```json
{
  "ok": true,
  "staff": { "id": "f1e2d3c4-b5a6-7890-1234-567890abcdef", "email": "staff@example.com" },
  "shops": ["demo-store.myshopify.com"],
  "shopNames": { "demo-store.myshopify.com": "Demo Store" }
}
```

| フィールド         | 説明                                                                                           |
| ------------- | -------------------------------------------------------------------------------------------- |
| `staff.id`    | スタッフのユーザー ID（routing write の操作者記録と同じ識別子）                                                     |
| `staff.email` | JWT の email claim（無い場合 `null`）                                                               |
| `shops`       | スタッフに束縛された shop ドメイン群（0 件は `[]`）                                                             |
| `shopNames`   | shop ドメイン → 表示名（ブランド名）の map（`login` と同形）。表示名が登録されている shop のみ含み、未登録の shop はアプリがドメインをそのまま表示します |

***

## `POST` /api/operator/account/delete

operator JWT を検証し、**ログイン中のスタッフ本人**のアカウントを即時削除します（アプリ内の退会導線）。削除できるのは常に JWT の持ち主自身だけで、他人の ID を指定する経路はありません。誤操作を防ぐため、リクエストボディに `confirm: true` が必須です。

削除すると、そのアカウントの全セッション・refresh token が即時失効し、shop 束縛も解除されます。出荷作業などの操作履歴に残る操作者 ID は匿名の UUID として保持されます（個人を特定するメールアドレスは削除されます）。

> **デモアカウントは削除できません。** 審査・デモ用に共有しているアカウントは保護されており `403 demo_account_protected` を返します。

### リクエスト

```http
POST https://api.lmile.io/api/operator/account/delete
Authorization: Bearer {OPERATOR_JWT}
Content-Type: application/json
```

```json
{
  "confirm": true,
  "source": "picking-ios"
}
```

### ボディパラメータ

| パラメータ     | 型       |  必須 | 説明                                                         |
| --------- | ------- | :-: | ---------------------------------------------------------- |
| `confirm` | boolean |  ✓  | `true` 固定。誤操作防止のため省略・`false` は `400 confirmation_required` |
| `source`  | string  |     | 削除の発生元メモ（監査用。例 `picking-ios`。最大 200 文字）                    |

### レスポンス

```json
{
  "ok": true,
  "deleted": true
}
```

| フィールド            | 説明                                            |
| ---------------- | --------------------------------------------- |
| `deleted`        | アカウントが削除されたら `true`                           |
| `alreadyDeleted` | 既に削除済みのアカウントへの再実行で `true`（冪等。フィールド自体が無い場合もある） |

### 挙動・冪等性

* **本人のみ**: 削除対象は JWT の `sub`（本人）。リクエストボディ等で他人を指定する手段はありません。
* **即時ハード削除**: アカウント（認証情報）を物理削除します。全セッション / refresh token は即時失効するため、削除後は同じ JWT で `/api/operator/*` を呼べません（`401`）。アプリ側はローカルのトークンを破棄してログイン画面に戻してください。
* **デモ保護**: デモアカウントは `403 demo_account_protected`。
* **監査**: 削除はメールアドレスのスナップショット付きで内部監査トレイルに記録されます（個人には非公開）。
* **冪等**: 既に削除済みのアカウント（有効だが失効済みの JWT を保持したケース等）に対しても `200` + `deleted: true` を返します。

## エラー

エンドポイント固有:

| Status | 条件                                                                                                  | レスポンス例                                                                                                          |
| :----: | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|  `401` | 認証失敗（`login`。メール存在有無は漏らさない）                                                                         | `{ "ok": false, "error_kind": "invalid_credentials", "error": "invalid_credentials" }`                          |
|  `401` | 確認コードが誤り / 期限切れ（`login/otp/verify`。原因詳細は漏らさない）                                                      | `{ "ok": false, "error_kind": "invalid_code", "error": "invalid_code" }`                                        |
|  `401` | ID token が不正 / 失効（`login/google` の Google ID token・`login/apple` の Apple identity token。原因詳細は漏らさない） | `{ "ok": false, "error_kind": "invalid_id_token", "error": "invalid_id_token" }`                                |
|  `401` | refresh token が不正 / 失効（`refresh`）                                                                   | `{ "ok": false, "error_kind": "invalid_refresh_token", "error": "invalid_refresh_token" }`                      |
|  `401` | JWT 未提示（`me`）                                                                                       | `{ "ok": false, "error_kind": "missing_token", "error": "Authorization: Bearer <jwt> required" }`               |
|  `401` | JWT が不正 / 失効（`me` / `account/delete`）                                                               | `{ "ok": false, "error_kind": "invalid_token", "error": "invalid or expired token" }`                           |
|  `400` | `confirm: true` 不在（`account/delete`）                                                                | `{ "ok": false, "error_kind": "confirmation_required", "error": "confirm:true is required to delete account" }` |
|  `403` | デモアカウントの削除（`account/delete`）                                                                        | `{ "ok": false, "error_kind": "demo_account_protected", "error": "demo account cannot be deleted" }`            |
|  `500` | 削除監査の記録に失敗（`account/delete`）                                                                        | `{ "ok": false, "error_kind": "audit_error", "error": "failed to record deletion" }`                            |
|  `429` | 試行回数の上限超過（`login` / `login/google` / `login/apple` / `refresh`）                                     | `{ "ok": false, "error_kind": "rate_limited", "error": "too many requests" }`                                   |
|  `503` | 認証基盤に到達できない                                                                                         | `{ "ok": false, "error_kind": "auth_unavailable", "error": "auth service unavailable" }`                        |

共通エラー（`400` / `405` / `500`）は [Error Codes](/lmile-docs/reference/error-codes.md) を参照。

## データモデル

* スタッフのアカウント（メールアドレス・パスワード・確認状態）は Supabase Auth（`auth.users`）が管理します。アカウントの作成はプラットフォーム / 管理者側で行い（アプリ内のセルフサインアップは廃止）、`login` / `login/google` / `login/apple` / `refresh` はトークンを発行します（本サービスの SoT テーブルには直接書き込みません）。
* `sot.lm_operator_staff_shops` — スタッフ（ユーザー）↔ shop の認可束縛 SoT。`/api/operator/*` の shop 認可（`login` / `refresh` が返す `shops`、各エンドポイントの `403 shop_not_authorized`）はこの束縛で判定します。発行されたばかりで未割当のアカウントは未束縛です（割当は Last Mile が行います）。スタッフのメールアドレス等を含むため `agent_read.*` / 公開 view には露出しません（内部管理のみ）。アカウント削除時はこの束縛も連動して解除されます。
* `sot.lm_operator_account_deletions` — `account/delete` の削除監査トレイル。削除した本人の ID・削除時点のメールアドレス snapshot・束縛 shop snapshot・削除時刻を記録します。メールアドレス（個人情報）を含むため `agent_read.*` / 公開 view には露出しません（内部監査のみ）。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://lmile.gitbook.io/lmile-docs/operator-api/login.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
