# 初始化、设备二维码与六位码登录

> HTML: https://remote.ivjn.us/docs/zh-CN/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 编码的设备名。
