# 初始化、裝置二維碼與六位碼登入

> HTML: https://remote.ivjn.us/docs/zh-TW/pairing/

Agent 預設 Exchange 為 `wss://exchange.ivjn.us`，CLI/Console 管理 API 地址為 `https://exchange.ivjn.us`。原有配置優先，不會覆蓋自建 Exchange 地址。

## SaaS 註冊與首次登入

```powershell
remote-cli register --username alice --email alice@example.com
# 按隐藏输入提示设置密码，注册后向邮箱发送验证码。
remote-cli verify-email --username alice
# 隐藏输入邮件中的 8 位数字验证码，然后登录。
remote-cli login --email alice@example.com
# init 是 login 的别名，不带 --email 时交互输入邮箱和密码。
remote-cli init
# 原有令牌仍可使用：
remote-cli login --server https://exchange.ivjn.us --token-stdin
```

註冊與登入預設連線 `https://exchange.ivjn.us`，自建服務可加 `--server`。登入以郵箱為賬號身份，地址會去空格並轉成小寫，格式不合法會直接拒絕。使用者名稱是註冊時的工作區名稱（3-64 位字母、數字或 `._-`）。帳號密碼不設長度或字元格式限制；伺服器僅儲存密碼雜湊。自動化可使用 `--password-stdin`，不要把密碼寫進命令引數。新賬號驗證郵箱前不能登入。CLI 只儲存返回的會話令牌，不儲存密碼，註冊等待驗證期間保留已有登入。

每次註冊建立獨立工作區，賬號是該工作區的管理員。裝置、成員、遠端會話、指標和審計均隔離，不同工作區可使用相同裝置 ID。六位碼批准會把裝置或新成員加入**批准人的工作區**。加入已有工作區應使用該管理員提供的令牌或六位碼授權。

Android Console 同樣支援註冊、密碼登入、已有令牌登入和六位碼登入。密碼只用於當前請求，憑證仍儲存到 SecureStore。

密碼登入會話有效期 30 天，每個賬號最多 32 個有效會話；重複登入不擠掉其他客戶端。`POST /api/auth/logout` 使用當前 Bearer 會話令牌登出，已建立的 WebSocket 最多 5 秒內關閉；到期後重新登入。裝置與原有運維令牌保留原生命週期。

純令牌身份沒有密碼帳號，不能透過郵箱找回。當前不包含計費或跨例項 WebSocket 路由。

## 郵箱驗證與密碼找回

```powershell
remote-cli resend-verification --email alice@example.com
remote-cli verify-email --username alice
remote-cli forgot-password --email alice@example.com
remote-cli reset-password --email alice@example.com
# 按提示输入邮件验证码、新密码，再重新登录。

# 已登录的旧密码账号绑定或更换邮箱，需要再次验证当前密码。
remote-cli set-email --email alice@example.com
remote-cli verify-email --username alice
```

驗證與找回使用不同用途的 8 位數字驗證碼，15 分鐘有效，連續輸錯 5 次後失效，重新發信會使舊碼失效；每個賬號每種用途一分鐘內只能發一次。驗證與重置命令可用 `--code` 自動化，預設隱藏輸入驗證碼。重置成功會撤銷該賬號的全部登入會話，已建立的 WebSocket 最多 5 秒內斷開；不影響工作區內其他成員和裝置。

Console 註冊頁填寫郵箱，隨後進入驗證頁；登入頁提供 **驗證郵箱** 和 **忘記密碼**。更換郵箱在驗證完成前保留原地址。重發驗證和找回請求返回統一提示，不暴露郵箱是否已註冊。

郵件功能需要遷移 `0004_email.sql` 和 SMTP 配置：`SMTP_HOST`、`SMTP_PORT`、`SMTP_FROM`、`SMTP_USERNAME`、`SMTP_PASSWORD`、`SMTP_SECURITY`。預設 STARTTLS，支援隱式 TLS；非加密 SMTP 僅允許本地測試迴環地址。SMTP 憑據放服務環境檔案，不提交到 Git。不配置郵件服務時，新版註冊和發信介面返回 503，既有賬號登入不受影響，因此生產啟用前必須完成發信配置。SMTP 暫時失敗可能留下待驗證賬號，恢復後使用重發驗證郵件，不必重複註冊。

## Exchange 賬號 API

| 方法與路徑 | 鑑權 | 用途 |
| --- | --- | --- |
| `POST /api/auth/register` | 無，限流 | `{username,password,email}`，建立待驗證賬號，返回 202 和 `verification_required`，不簽發會話 |
| `POST /api/auth/login` | 無，限流 | `{email,password}`，簽發獨立登入會話 |
| `GET /api/auth/me` | 運維令牌或登入會話 | 返回 `name`、`tenant_id`、`role` |
| `POST /api/auth/logout` | 當前登入會話 | 撤銷當前會話；不撤銷其他客戶端 |
| `POST /api/auth/verify-email` | 無，限流 | `{username,code}`，消費郵箱驗證碼 |
| `POST /api/auth/resend-verification` | 無，限流 | `{email}`，向待驗證地址重發郵件 |
| `POST /api/auth/forgot-password` | 無，限流 | `{email}`，向已驗證地址傳送找回驗證碼 |
| `POST /api/auth/reset-password` | 無，限流 | `{email,code,password}`，修改密碼並撤銷舊會話 |
| `POST /api/auth/email` | 當前登入會話和賬號密碼 | `{email,password}`，申請繫結或更換郵箱，驗證後生效 |

登入成功返回 `username`、`email`（若已繫結）、`tenant_id`、`token`、`expires_in`，並設定 `Cache-Control: no-store`。賬號密碼使用 Argon2id 預設引數加鹽儲存；每個 TCP 來源 IP 每分鐘最多 20 次賬號請求，每個使用者名稱每分鐘最多 10 次，密碼雜湊併發最多 4 個。不信任客戶端提供的轉發 IP；反向代理後的客戶端共享 IP 配額，公共部署應在可信入口配置額外限流。

Rust Exchange 預設關閉公開註冊。須明確設定 `EXCHANGE_ALLOW_REGISTRATION=true` 並設定 SMTP 才能啟用；`false` 關閉新註冊，已有帳號仍可登入。

## 讓 Agent 顯示六位碼

Android Remote Agent：預設地址已填好，點選 **Connect with a six-digit code**。應用會顯示一個 5 分鐘有效的六位碼。在已登入的管理員 CLI 上執行：

```powershell
remote-cli pair 012345
# 可选：覆盖默认 ID 和显示名称
remote-cli pair 012345 --device-id my-android --name "我的手机"
```

CLI 獲取連線碼時顯示裝置名、身份型別和建議裝置 ID，確認是自己的裝置後授權。預設 ID 使用裝置名的小寫 ASCII 字母、數字和 `._-`，去掉空格及其他字元；無可用字元時使用 `device`，最多 64 字元。同一工作區重名時依次追加數字，例如 `realmev5`、`realmev51`、`realmev52`，必要時截短基礎 ID。Exchange 在建立時處理併發衝突，CLI 列印最終分配的 ID；手動指定的 ID 重名也追加數字。Android 使用系統裝置名（不可用時使用型號），原生 Agent 使用主機名。Agent 自動取得獨立裝置令牌、加密儲存並啟動連線。不會自動開啟輔助功能或螢幕控制。取消、過期或鑑權失敗不會替換已有配置；Agent 正在連線時需先停止。

## 電腦、路由器與 Linux Agent 新增裝置

Windows、macOS、OpenWrt、Linux Agent 無需手工填寫裝置令牌，任選一種方式：

| 平臺 | 方式一：配對碼 / 裝置二維碼 | 方式二：貼上邀請 |
| --- | --- | --- |
| Windows / macOS（桌面 `--ui`，雙擊啟動） | 本地頁面 **Add this device → Get pairing code**，顯示六位碼和二維碼 | 本地頁面 **Invite** 文字框貼上後點 **Join with invite** |
| OpenWrt（luci-app-remote） | LuCI **服務 → 遠端管理 → 新增裝置 → 獲取配對碼**，顯示六位碼和二維碼 | 同頁 **方式二：邀請內容** 貼上後點 **加入** |
| Linux / 所有平臺命令列 | `remote-agent pair`（終端顯示六位碼和字元二維碼） | `remote-agent join '<邀请内容>'`，或 `remote-agent join -` 從標準輸入讀取 |

**方式一**：Agent 申請 5 分鐘有效的六位碼，同時顯示二維碼，內容為 `remote://pair?code=012345&server=wss%3A%2F%2Fexchange.ivjn.us`（不含任何憑證）。管理員任選：
Android Console **新增裝置 → 輸入配對碼 → 掃描裝置二維碼**（相機掃描，核對服務端一致後查詢並確認批准）；或輸入六位碼；Web Console **批准配對碼** 可輸入六位碼或貼上 `remote://pair?...`；或 `remote-cli pair 012345`（也接受該 URI）。批准後 Agent 自動儲存令牌並連線。

**方式二**：管理員在 Android Console **新增裝置 → 掃碼新增** 生成一次性邀請，點選 **複製邀請（電腦 / 路由器貼上）**；或執行 `remote-cli qr --device-id pc-01 --text`。邀請是二維碼中的 JSON（`{"type":"remote-enrollment",...}`），5 分鐘內有效、只能使用一次。也接受裝置令牌二維碼（`remote-cli device qr`）的內容。

儲存位置與連線：

- 桌面 UI：寫入當前配置檔案（預設 `%LOCALAPPDATA%\remote-agent\config.json` / `~/.config/remote-agent/config.json`）並立即連線。
- OpenWrt：寫入 UCI `remote-agent.main`（server / device_id / token，`enabled=1`），再重啟 procd 服務。
- 命令列：預設寫入桌面配置路徑（OpenWrt 預設寫 UCI），`--config PATH` 指定 JSON 檔案，`--uci` 強制寫 UCI；儲存後在前臺連線（`--no-connect` 只儲存）。如已有服務在用該配置，請重啟服務。

已配置憑證時預設拒絕替換：命令列需 `--force`，桌面 UI 與 LuCI 會先確認。已有的令牌方式（`--device-id/--token`、配置檔案、LuCI 設定頁）不變。

```sh
remote-agent pair                       # 显示六位码 + 二维码，批准后保存并连接
remote-agent pair --server wss://your-exchange.example.com --config ./agent.json
remote-agent join '{"type":"remote-enrollment","version":1,...}'
remote-agent join - < invite.txt --no-connect
```

原生配對要求 `wss://`，僅本機迴環允許 `ws://` 以便測試；Android 和 Console 配對要求 HTTPS/WSS。

## Console 六位碼登入

Android Remote Console 登入頁點選 **使用六位碼登入**；Web Console（`/console`）登入卡片選擇 **六位碼** 標籤，點選獲取登入碼。管理員可任選其一批准：

```powershell
remote-cli pair 012345 --operator-name mobile-alice
# 只读客户端：追加 --role readonly
```

或在已登入的 **Web Console** 工作空間點選 **批准配對碼**（僅管理員可見；只讀操作員無此入口），輸入對方螢幕上的六位碼，核對型別與標籤後批准。

預設建立獨立 `operator` 賬號，不復制管理員自身的令牌。管理員可明確指定 `--role admin` 或 `--role readonly`。Android Console 收到授權後再次驗證憑證，儲存到 SecureStore 並進入裝置列表；Web Console 將操作員憑證保留在當前頁面記憶體中（與賬號 / 令牌登入相同），進入裝置列表。取消或失敗時仍可使用原有賬號或令牌登入，不會清掉後續其他登入方式的輸入。

## Remote Agent 掃 CLI 碼登入

```powershell
remote-cli qr --device-id my-phone
# 输出 SVG 供手机扫描：
remote-cli qr --device-id my-phone --output ./phone-login.svg
```

在已登入管理員 CLI 上生成二維碼，手機 Remote Agent 點選 **Scan connection QR code**，確認服務端和裝置 ID 後點選 **Connect**。二維碼只攜帶隨機 256 位臨時票據，5 分鐘有效，只能繫結一臺裝置，不含長期裝置令牌。掃描後由 Exchange 簽發裝置令牌，手機透過 Keystore 儲存並連線。二維碼對應的裝置 ID 必須是新的；不會替換或輪換已有裝置。

相同手機領取請求攜帶固定隨機 `claim_id`，網路斷開後重試可以取回同一結果；不同 `claim_id` 不能重複領取。儲存後手機確認清除票據。臨時票據在程序記憶體中儲存，Exchange 重啟會失效；已開始領取的結果另保留最多 5 分鐘供原客戶端重試。

| 方法與路徑 | 鑑權 | 用途 |
| --- | --- | --- |
| `POST /api/pairing/invite` | 管理員 | `{device_id,name?}`，生成票據，限制每管理員每分鐘 10 次、全域性 256 個 |
| `POST /api/pairing/claim` | `Bearer <ticket>` | `{claim_id}`，返回獨立裝置憑證；重試必須沿用同一 32 位十六進位制 `claim_id` |
| `POST /api/pairing/claim/complete` | 同上 | 儲存成功後確認清除 |

## 生成裝置二維碼

```powershell
# 创建设备，同时在终端显示二维码
remote-cli device add my-android --qr
# 新建 SVG 文件，可在电脑打开后供手机扫描
remote-cli device add another-phone --qr-output ./another-phone.svg
# 使用手里已有的设备令牌，不创建或轮换设备
remote-cli device qr my-android --output ./my-android.svg
# 明确轮换令牌并生成二维码；原令牌立即失效
remote-cli device rotate-token my-android --qr
```

`device qr` 以隱藏輸入讀取裝置令牌，也支援 `--token-stdin`。裝置二維碼只包含裝置令牌，不能使用運維令牌。輸出檔案拒絕覆蓋，包含憑證，應妥善保管並在使用後刪除。二維碼要求 HTTPS/WSS。

Android Console：建立新裝置後會直接顯示裝置二維碼；裝置詳情的 **輪換令牌並生成二維碼** 會先說明斷線影響並要求確認。服務端只儲存令牌雜湊，不能從已存在的裝置記錄恢復舊二維碼，因此不會為了檢視二維碼而靜默輪換令牌。

## Exchange 配對 API

| 方法與路徑 | 鑑權 | 用途 |
| --- | --- | --- |
| `POST /api/pairing/start` | 無，限流 | 提交 `kind: device/operator`、`label`，取得 `code`、`poll_token`、`expires_in: 300`、`interval: 3` |
| `GET /api/pairing/{code}` | 管理員運維令牌 | 檢視申請型別、標籤和剩餘時間，不返回憑證 |
| `POST /api/pairing/approve` | 管理員運維令牌 | 提交 `code`、`kind`、`identity`、可選 `name`、`role`，建立獨立裝置或賬號 |
| `POST /api/pairing/poll` | `Authorization: Bearer <poll_token>` | 返回 `pending` 或 `approved`；批准結果含 `credentials.kind/identity/token` |
| `POST /api/pairing/complete` | 同上 | 客戶端儲存成功後清除臨時授權；重複呼叫安全 |
| `GET /ws/pairing` | 無，限流 | 原生 Agent 配對 WebSocket，依次接收 `code`、`approved` 或 `expired` 事件，儲存後傳送文字 `complete` |

六位碼只用來讓已認證管理員定位並確認請求，不能用來輪詢或換取憑證。輪詢憑證為隨機 256 位秘密，不放入 URL。重複批准被拒絕；身份建立失敗會釋放授權佔用以便重試。批准結果可在同一私有輪詢憑證下重讀，避免一次網路丟包導致憑證永久丟失；客戶端儲存後確認清除。

等待授權與批准待領取分別最多保留 5 分鐘。未配置 `REDIS_URL` 時，臨時會話只儲存在當前 Exchange 程序記憶體中，重啟後需重新領碼，多例項需把配對請求固定到同一例項。配置了 Redis 後，配對條目與限流計數寫入 Redis（與 5 分鐘 TTL 對齊），多例項可任意分發、**不必**粘性。每個實際 TCP 來源 IP 每分鐘最多領取 10 次，管理員每分鐘最多查詢或批准 20 次；記憶體模式另有全域性最多 256 個待處理會話的上限。代理轉發的客戶端 IP 不作為可信限流依據；同一反向代理後的使用者共享來源額度。過期資料由 TTL/定時清理清除，響應設定 `Cache-Control: no-store`，不記錄配對秘密或簽發令牌明文。

後端已有裝置 ID/賬號不能被配對請求覆蓋。管理員應核對程式碼和申請端；不要批准他人誘導提供的登入碼。

連線碼 API：`POST /api/pairing/start` 可提交可選 `device_name`，響應返回該裝置名。管理員 `GET /api/pairing/{code}` 返回 `device_name` 和當前工作區的 `suggested_device_id`，查詢不會佔用 ID。`POST /api/pairing/approve` 的裝置 `identity` 可省略，成功返回 200 和 `{identity}`（最終 ID，不含令牌）。原生 WebSocket `/ws/pairing?device_name=...` 支援 URL 編碼的裝置名。
