Improve youth mode action token flow
This commit is contained in:
+12
-8
@@ -759,11 +759,11 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
|
||||
**状态:新增。**
|
||||
|
||||
- 敏感操作先调用 `POST /v1/im/security/challenges`,Body `{"scene":"close_youth_mode","channel":"sms"}`;手机号由登录身份读取,客户端不提交目标手机号。
|
||||
- 敏感操作先调用 `POST /v1/im/security/challenges`;开启青少年模式设置独立密码时 Body 为 `{"scene":"set_independent_password","channel":"sms"}`,关闭青少年模式时 Body 为 `{"scene":"close_youth_mode","channel":"sms"}`。手机号由登录身份读取,客户端不提交目标手机号。
|
||||
- 发码/校验:服务端返回 `challenge_id` 和脱敏手机号;提交 `POST /v1/im/security/challenges/{id}/verify`,Body `{"code":"..."}`。
|
||||
- 重复发码:同一用户、同一手机号、同一敏感场景存在未过期 challenge 且仍在 `resend_after_seconds` 冷却期内时,服务端返回 HTTP `429 VERIFICATION_RATE_LIMITED`,`details` 带 `challenge_id` 和 `retry_after_seconds`。客户端应复用该 `challenge_id` 继续倒计时,不要当成接口不可达或重新登录。
|
||||
- 小时限流:超过一小时创建次数上限时同样返回 HTTP `429 VERIFICATION_RATE_LIMITED`,`details.retry_after_seconds` 可能大于 60 秒且不一定带 `challenge_id`。客户端只展示倒计时,倒计时结束后允许用户重新获取验证码。
|
||||
- 成功返回 `action_token`、`action`、`expires_at`;后续敏感接口以 `X-Action-Token` 携带,Token 只能用于指定动作且单次消费。
|
||||
- 成功返回 `action_token`、`action`、`expires_at`;Token 只能用于指定动作且单次消费。设置独立密码接口通过 Body `action_token` 携带;关闭青少年模式接口通过 Header `X-Action-Token` 携带。
|
||||
- 错误:`VERIFICATION_RATE_LIMITED`、`CHALLENGE_EXPIRED`、`CODE_INVALID`、`TOO_MANY_ATTEMPTS`;前端不得把短信验证码或 action token 落普通存储。
|
||||
|
||||
### 1.8 实名认证.png
|
||||
@@ -803,8 +803,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
|
||||
**状态:新增。**
|
||||
|
||||
- 设置:`POST /v1/im/settings/independent-password`,Body `{"password":"1234","purpose":"youth_mode","action_token":"..."}`;服务端只存强哈希,不返回密码。
|
||||
- 设置:`POST /v1/im/settings/independent-password`,Body `{"password":"1234","purpose":"youth_mode","action_token":"..."}`;服务端只存强哈希,不返回密码。`action_token` 为一次性 token,会在设置密码时消费;设置成功后响应会返回新的 `enable_action_token`,用于下一步开启青少年模式,前端不要复用设置密码时提交的 token。
|
||||
- 验证:`POST /v1/im/settings/independent-password/verify` 返回短时 `action_token`,用于关闭青少年模式或修改密码。
|
||||
- 青少年模式完整开启和关闭流程见 `docs/IM-青少年模式前后端对接文档.md`。
|
||||
- 规则:四位数字、禁止明显连续/重复组合;连续错误返回 `TOO_MANY_ATTEMPTS` 和 `retry_after_seconds`。前端输入框禁截屏/自动填充,离开页面立即清空。
|
||||
- 忘记密码:跳转 1.7 的短信身份验证,验证后重置,不提供明文找回。
|
||||
|
||||
@@ -2402,7 +2403,8 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
**状态:新增。**
|
||||
|
||||
- `GET /v1/im/settings/youth-mode` 返回 `enabled,policy_version,restrictions,password_set,can_enable`。
|
||||
- 点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用 `POST /v1/im/settings/youth-mode/enable`,Body `{"policy_version":n,"action_token":"..."}`。
|
||||
- 点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用 `POST /v1/im/settings/youth-mode/enable`,Body `{"policy_version":n,"action_token":"..."}`,其中 `action_token` 使用设置独立密码响应里的 `enable_action_token`。
|
||||
- 开启链路中,短信校验返回的 `set_independent_password` token 只用于设置独立密码;设置密码成功响应中的 `enable_action_token` 才用于 `youth-mode/enable`。
|
||||
- 服务端持久化到用户租户范围并推送所有设备,不能仅设置本地标志。
|
||||
|
||||
### 10.15 青少年模式提示(安卓).png
|
||||
@@ -2491,13 +2493,15 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true}}` |
|
||||
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"status":"ok","password_set":true,"purpose":"youth_mode"}` |
|
||||
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"status":"ok","password_set":true,"purpose":"youth_mode","enable_action_token":"enable_token","enable_action_expires_at":"2026-08-20T09:05:00Z","next_action":"enable_youth_mode"}` |
|
||||
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
|
||||
| `POST /v1/im/settings/youth-mode/enable` | `{"policy_version":1,"action_token":"action_token"}` | `{"enabled":true,"policy_version":1,"password_set":true,"restrictions":["night_quiet","content_filter","payment_guard"],"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||
| `POST /v1/im/settings/youth-mode/enable` | `{"policy_version":1,"action_token":"enable_action_token"}` | `{"enabled":true,"policy_version":1,"password_set":true,"restrictions":["night_quiet","content_filter","payment_guard"],"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||
| `POST /v1/im/settings/youth-mode/disable` | Header `X-Action-Token: action_token`;Body `{}` | `{"enabled":false,"policy_version":1,"password_set":true,"updated_at":"2026-08-20T09:00:00Z"}` |
|
||||
| `POST /v1/im/security/challenges` | Header `Idempotency-Key`;Body `{"scene":"close_youth_mode","channel":"sms"}` | `{"challenge_id":"sec_chg_1","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60}` |
|
||||
| `POST /v1/im/security/challenges` 开启青少年模式 | Header `Idempotency-Key`;Body `{"scene":"set_independent_password","channel":"sms"}` | `{"challenge_id":"sec_chg_1","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60}` |
|
||||
| `POST /v1/im/security/challenges` 关闭青少年模式 | Header `Idempotency-Key`;Body `{"scene":"close_youth_mode","channel":"sms"}` | `{"challenge_id":"sec_chg_2","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60}` |
|
||||
| `POST /v1/im/security/challenges` 限流 | 同上 | HTTP `429`:`{"code":30020,"reason":"VERIFICATION_RATE_LIMITED","message":"verification rate limited","details":{"challenge_id":"sec_chg_1","retry_after_seconds":45}}`;小时限流时 `retry_after_seconds` 可能为数百/数千秒且可不返回 `challenge_id` |
|
||||
| `POST /v1/im/security/challenges/{id}/verify` | `{"code":"123456"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
|
||||
| `POST /v1/im/security/challenges/{id}/verify` 开启青少年模式 | `{"code":"123456"}` | `{"action_token":"set_password_token","action":"set_independent_password","expires_at":"2026-08-20T09:05:00Z"}` |
|
||||
| `POST /v1/im/security/challenges/{id}/verify` 关闭青少年模式 | `{"code":"123456"}` | `{"action_token":"close_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
|
||||
| `GET /v1/im/security/realname` | Query only | `{"status":"unverified","can_submit":true}` |
|
||||
| `POST /v1/im/security/realname/submissions` | `{"name":"张三","id_type":"cn_id","id_number":"110101199001011234"}` | `{"submission_id":"rn_1","status":"pending","can_submit":false}` |
|
||||
| `POST /v1/im/security/realname/appeals` 待实现 | `{"reason":"证件变更","description":"需要更正实名信息","action_token":"action_token"}` | `{"appeal_id":"rna_1","status":"pending","submitted_at":"2026-08-20T09:00:00Z"}` |
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
# IM 青少年模式前后端对接文档
|
||||
|
||||
## 1. 基本约定
|
||||
|
||||
青少年模式相关接口均为登录后接口。客户端需要携带用户登录 token、普通 API Key 和协议版本头。
|
||||
|
||||
公共 Header:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
X-MeetingIM-Auth: Bearer <access_token>
|
||||
X-MeetingIM-APIKey: <api_key>
|
||||
X-IM-Schema: 1
|
||||
X-Request-ID: <uuid>
|
||||
X-Tenant-Code: default
|
||||
```
|
||||
|
||||
所有写接口还需要携带:
|
||||
|
||||
```http
|
||||
Idempotency-Key: <uuid>
|
||||
```
|
||||
|
||||
测试环境验证码固定为 `123456`。`action_token` 为 5 分钟有效、单次消费,只能用于指定动作,不能在多个接口之间重复使用。
|
||||
|
||||
## 2. 开启青少年模式
|
||||
|
||||
### 2.1 查询当前状态
|
||||
|
||||
```http
|
||||
GET /v1/im/settings/youth-mode
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"enabled": false,
|
||||
"policy_version": 1,
|
||||
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
|
||||
"password_set": false,
|
||||
"can_enable": false,
|
||||
"updated_at": "2026-08-20T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端使用 `policy_version` 和 `restrictions` 渲染开启确认页。最终开启时需要提交用户确认过的 `policy_version`。
|
||||
|
||||
### 2.2 发起短信身份验证
|
||||
|
||||
```http
|
||||
POST /v1/im/security/challenges
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"scene": "set_independent_password",
|
||||
"channel": "sms"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"challenge_id": "sec_chg_1",
|
||||
"phone_mask": "+861****000",
|
||||
"expires_in_seconds": 300,
|
||||
"resend_after_seconds": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
手机号由服务端从当前登录用户读取,客户端不提交目标手机号。
|
||||
|
||||
### 2.3 校验短信验证码
|
||||
|
||||
```http
|
||||
POST /v1/im/security/challenges/{challenge_id}/verify
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"action": "set_independent_password",
|
||||
"action_token": "set_password_token",
|
||||
"expires_at": "2026-08-20T09:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里拿到的 `action_token` 只用于设置独立密码,不能直接用于开启青少年模式。
|
||||
|
||||
### 2.4 设置独立密码
|
||||
|
||||
```http
|
||||
POST /v1/im/settings/independent-password
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"password": "6439",
|
||||
"purpose": "youth_mode",
|
||||
"action_token": "set_password_token"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": "ok",
|
||||
"password_set": true,
|
||||
"purpose": "youth_mode",
|
||||
"enable_action_token": "enable_token",
|
||||
"enable_action_expires_at": "2026-08-20T09:05:00Z",
|
||||
"next_action": "enable_youth_mode"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端会消费 `set_password_token`,并返回新的 `enable_action_token`。前端下一步必须使用 `enable_action_token`,不要复用 `set_password_token`。
|
||||
|
||||
独立密码规则:
|
||||
|
||||
- 必须是 4 位数字。
|
||||
- 不能是明显连续或重复组合。
|
||||
- 服务端只保存强哈希,不返回明文密码。
|
||||
- 前端离开页面后应清空输入内容,不落普通存储。
|
||||
|
||||
### 2.5 开启青少年模式
|
||||
|
||||
```http
|
||||
POST /v1/im/settings/youth-mode/enable
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"policy_version": 1,
|
||||
"action_token": "enable_token"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"policy_version": 1,
|
||||
"password_set": true,
|
||||
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
|
||||
"updated_at": "2026-08-20T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
开启成功后,客户端应刷新本地设置页和 `bootstrap` 缓存。服务端会持久化状态,前端不能只依赖本地标志位。
|
||||
|
||||
## 3. 关闭青少年模式
|
||||
|
||||
### 3.1 校验独立密码
|
||||
|
||||
```http
|
||||
POST /v1/im/settings/independent-password/verify
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"password": "6439",
|
||||
"purpose": "youth_mode",
|
||||
"action": "close_youth_mode"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"action": "close_youth_mode",
|
||||
"action_token": "close_token",
|
||||
"expires_at": "2026-08-20T09:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 关闭模式
|
||||
|
||||
```http
|
||||
POST /v1/im/settings/youth-mode/disable
|
||||
X-Action-Token: close_token
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"enabled": false,
|
||||
"policy_version": 1,
|
||||
"password_set": true,
|
||||
"updated_at": "2026-08-20T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关闭接口的 `action_token` 通过 Header `X-Action-Token` 传递,不放在 Body 中。
|
||||
|
||||
## 4. Token 使用规则
|
||||
|
||||
| 来源 | action | 用途 | 传递方式 | 是否单次消费 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `security/challenges/{id}/verify` | `set_independent_password` | 设置独立密码 | Body `action_token` | 是 |
|
||||
| `independent-password` | `enable_youth_mode` | 开启青少年模式 | Body `action_token` | 是 |
|
||||
| `independent-password/verify` | `close_youth_mode` | 关闭青少年模式 | Header `X-Action-Token` | 是 |
|
||||
|
||||
常见错误:
|
||||
|
||||
- `ACTION_TOKEN_INVALID`:token 已消费、过期、action 不匹配、target 不匹配或用户不匹配。
|
||||
- `CODE_INVALID`:短信验证码错误。
|
||||
- `CHALLENGE_EXPIRED`:短信 challenge 已过期。
|
||||
- `TOO_MANY_ATTEMPTS`:独立密码或验证码错误次数过多。
|
||||
- `VERIFICATION_RATE_LIMITED`:发码过于频繁,按 `retry_after_seconds` 倒计时。
|
||||
|
||||
## 5. curl 示例
|
||||
|
||||
开启流程:
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/security/challenges' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
--data '{"scene":"set_independent_password","channel":"sms"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/security/challenges/<challenge_id>/verify' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
--data '{"code":"123456"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/settings/independent-password' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
--data '{"password":"6439","purpose":"youth_mode","action_token":"<set_password_token>"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/settings/youth-mode/enable' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
--data '{"policy_version":1,"action_token":"<enable_action_token>"}'
|
||||
```
|
||||
|
||||
关闭流程:
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/settings/independent-password/verify' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
--data '{"password":"6439","purpose":"youth_mode","action":"close_youth_mode"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -sS 'https://api.118im.com/v1/im/settings/youth-mode/disable' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
|
||||
-H 'X-MeetingIM-APIKey: <api_key>' \
|
||||
-H 'X-IM-Schema: 1' \
|
||||
-H 'X-Tenant-Code: default' \
|
||||
-H 'X-Request-ID: <uuid>' \
|
||||
-H 'Idempotency-Key: <uuid>' \
|
||||
-H 'X-Action-Token: <close_token>' \
|
||||
--data '{}'
|
||||
```
|
||||
+15
-3
@@ -3021,7 +3021,7 @@ func serveIMYouthModeEnable(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, strings.TrimSpace(body.ActionToken), "set_independent_password", "youth_mode", time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, strings.TrimSpace(body.ActionToken), "enable_youth_mode", "youth_mode", time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30042, imReasonActionToken, "action token invalid", nil)
|
||||
return
|
||||
}
|
||||
@@ -6081,7 +6081,19 @@ func serveIMIndependentPassword(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok", "purpose": purpose})
|
||||
enableToken, enableExpiresAt, err := issueIMActionToken(principal.TenantID, principal.Uid, "enable_youth_mode", purpose)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"status": "ok",
|
||||
"password_set": true,
|
||||
"purpose": purpose,
|
||||
"enable_action_token": enableToken,
|
||||
"enable_action_expires_at": enableExpiresAt.Format(time.RFC3339Nano),
|
||||
"next_action": "enable_youth_mode",
|
||||
})
|
||||
}
|
||||
|
||||
func serveIMIndependentPasswordVerify(wrt http.ResponseWriter, req *http.Request) {
|
||||
@@ -6338,7 +6350,7 @@ func issueIMActionToken(tenantID types.TenantID, uid types.Uid, action, target s
|
||||
|
||||
func defaultIMActionTarget(action, fallback string) string {
|
||||
switch action {
|
||||
case "set_independent_password", "close_youth_mode", "reset_independent_password":
|
||||
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
|
||||
return "youth_mode"
|
||||
default:
|
||||
return fallback
|
||||
|
||||
+12
-2
@@ -2132,6 +2132,13 @@ func TestServeIMIndependentPasswordSetAndVerify(t *testing.T) {
|
||||
bcrypt.CompareHashAndPassword(security.independentPassword.PasswordHash, []byte("2794")) != nil {
|
||||
t.Fatalf("password was not stored as a valid hash: %#v", security.independentPassword)
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["password_set"] != true || data["next_action"] != "enable_youth_mode" || data["enable_action_token"] == "" {
|
||||
t.Fatalf("unexpected set response: %#v", data)
|
||||
}
|
||||
if security.actionToken == nil || security.actionToken.Action != "enable_youth_mode" || security.actionToken.Target != "youth_mode" {
|
||||
t.Fatalf("unexpected enable token after set: %#v", security.actionToken)
|
||||
}
|
||||
|
||||
expectIMBearer(t, ctrl, "token-verify", 7, 42)
|
||||
req = httptest.NewRequest(http.MethodPost, "/v1/im/settings/independent-password/verify",
|
||||
@@ -2144,7 +2151,7 @@ func TestServeIMIndependentPasswordSetAndVerify(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("verify status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
data = decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["action"] != "close_youth_mode" || data["action_token"] == "" {
|
||||
t.Fatalf("unexpected verify response: %#v", data)
|
||||
}
|
||||
@@ -3156,7 +3163,7 @@ func TestServeIMYouthModeEnableDisable(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
oldSecurity := store.Security
|
||||
security := &testSecurityStore{
|
||||
actionToken: &types.ActionToken{Action: "set_independent_password", Target: "youth_mode"},
|
||||
actionToken: &types.ActionToken{Action: "enable_youth_mode", Target: "youth_mode"},
|
||||
independentPassword: &types.IndependentPassword{PasswordHash: []byte("hash")},
|
||||
}
|
||||
store.Security = security
|
||||
@@ -3172,6 +3179,9 @@ func TestServeIMYouthModeEnableDisable(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("enable status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if security.consumedAction != "enable_youth_mode" || security.consumedTarget != "youth_mode" {
|
||||
t.Fatalf("unexpected enable token consumption: action=%q target=%q", security.consumedAction, security.consumedTarget)
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["enabled"] != true || data["policy_version"].(float64) != 2 {
|
||||
t.Fatalf("unexpected youth enable payload: %#v", data)
|
||||
|
||||
@@ -223,6 +223,8 @@ type testSecurityStore struct {
|
||||
realName *types.RealNameVerification
|
||||
realNameAppeal *types.RealNameAppeal
|
||||
actionToken *types.ActionToken
|
||||
consumedAction string
|
||||
consumedTarget string
|
||||
independentPassword *types.IndependentPassword
|
||||
youthMode *types.YouthModeSettings
|
||||
attempts int
|
||||
@@ -294,6 +296,8 @@ func (s *testSecurityStore) CreateActionToken(_ types.TenantID, token *types.Act
|
||||
}
|
||||
|
||||
func (s *testSecurityStore) ConsumeActionToken(_ types.TenantID, _ types.Uid, _ string, action string, target string, _ time.Time) (*types.ActionToken, error) {
|
||||
s.consumedAction = action
|
||||
s.consumedTarget = target
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user