v61:新增账号自助接口(GET /account + POST /account/password)

端上新增「个人设置」页要用:用户 2026-10-19 要求把抽屉里的
「通知消息 / 个人设置 / 修改密码 / 系统设置」都收进"点右上角头像"那一页,
所以端上必须能自己改密码,不能再指着用户去网页改。

  GET  /api/v1/account            → 我的资料 + 账号信息 + 当前设备
  POST /api/v1/account/password   → 改密码(old_password/password/password2)

规则与网页那条改密码路由逐条对齐(不能端上比网页松):
  · 必须验旧密码;
  · 两次新密码一致;
  · 过 Account::validatePassword()(≥8 位 + 字母数字都要有)。
额外收紧一条:新密码不能与原密码相同(网页没拦,端上拦了没坏处)。

两处刻意为之:
  · 改密码**不查写权限**(网页那条路由只要求登录,guest 也能改自己的密码)
    —— 加了写权限检查就会出现"网页能改、App 不能改"的权限裂缝;
  · 改完**不动现有令牌** —— 用户在自己手机上改密码不该被踹下线。

features 声明补 account / account_password,端上可探测。

验证:_fb_v61_account_api.py 20/0(独立副本 + 自检 + 随机端口 + 真 MariaDB,
覆盖 5 种错误路径 + 改完旧密码登不进/新密码能登/原令牌仍有效);
php -l 全量通过;线上部署已完成。
This commit is contained in:
2026-10-19 12:19:06 +08:00
parent 97e963e2ee
commit ad7f43af68
+102
View File
@@ -41,6 +41,11 @@ final class ApiHandler
// v57:登录即"一台设备一个会话",`/me` 滑动续期 24h,
// `/devices` 可在网页端/端上踢设备
'device_session' => 1,
/* 🔴 v61:账号自助 —— 端上「个人设置」页要能改密码、
看自己这台设备。`/account` 返回 profile + 当前设备,
`POST /account/password` 改密码。 */
'account' => 1,
'account_password' => 1,
'home_memories' => 1,
// v58:同步可以"先问一句哪些模块变了",只拉变了的
// (见 `changes()` 的说明;老端上没有这个标志也不会坏)
@@ -85,6 +90,11 @@ final class ApiHandler
// App 是 Bearer 令牌那套,没有会话;而原先前端只能让人
// **手动去网页签发令牌再粘贴**到 App 里 —— 这个体验没法接受。
case 'login': $this->login($method); return;
/* 🔴 v61:账号自助(端上「个人设置」页)。
⚠️ 与网页那条改密码路由**同一套规则**:
必须验旧密码、两次新密码一致、且过 `Account::validatePassword()`
(≥8 位 + 字母与数字都有)—— 端上不该比网页松。 */
case 'account': $this->account($sub, $method); return;
}
Api::fail('未知的接口资源:' . $res, 'not_found', 404);
@@ -267,6 +277,98 @@ final class ApiHandler
]);
}
/**
* 账号自助(端上「个人设置」页)。
*
* GET /api/v1/account → 我的资料 + 账号信息 + 当前设备
* POST /api/v1/account/password → 改密码(old_password / password / password2)
*
* 🔴 v61(用户 2026-10-19):「左侧抽屉,把通知消息、个人设置、修改密码、
* 系统设置,都去掉,放在个人设置页,就是在点击右侧头像那里。」
* ⇒ 端上个人设置页要能**自己改密码**,不能再指着用户去网页改。
*
* ⚠️ 规则与网页那条路由**逐条对齐**(不能端上比网页松):
* · 必须验旧密码;
* · 两次新密码必须一致;
* · 过 `Account::validatePassword()`(≥8 位 + 字母数字都要有)。
* ⚠️ 新密码**不能与原密码相同**:网页那边没拦,但端上自己拦一道没坏处
* (用户改完发现"还是旧密码"会以为没保存成功)。
* ⚠️ 改完**不动现有令牌**:用户在自己的手机上改密码,不该把自己踹下线
* (其它设备上的会话仍有效 —— 要踢设备请用 `/devices`)。
*/
private function account(string $sub, string $method): void
{
$act = Api::actor();
$pid = (int)$act['person_id'];
if ($sub === 'password') {
if ($method !== 'POST') {
Api::fail('改密码要用 POST。', 'method_not_allowed', 405);
}
/* ⚠️ 这里**刻意不做写权限校验**:
只读访客(guest)在网页上也能改自己的密码
(那条路由只要求登录,不查写权限)。
端上若加了写权限检查,就会出现"网页能改、App 不能改"的权限裂缝。 */
$old = (string)Api::param('old_password', '');
$new = (string)Api::param('password', '');
$new2 = (string)Api::param('password2', '');
$acc = Account::findByPerson($pid);
if ($acc === null) {
Api::fail('这个账号不存在(可能只有成员资料、还没开通登录)。', 'not_found', 404);
}
if (!password_verify($old, (string)$acc['password_hash'])) {
// 只回"当前密码不对",不回其它线索
Api::fail('当前密码不对。', 'unauthorized', 401);
}
if ($new !== $new2) {
Api::fail('两次输入的新密码不一致。', 'unprocessable', 422);
}
if ($new === $old) {
Api::fail('新密码不能和当前密码一样。', 'unprocessable', 422);
}
$errs = Account::validatePassword($new);
if (count($errs) > 0) {
Api::fail(implode(';', $errs), 'unprocessable', 422);
}
Account::setPassword((int)$acc['id'], $new);
Api::ok(['changed' => 1]);
}
if ($method !== 'GET') {
Api::fail('不支持的方法。', 'method_not_allowed', 405);
}
$p = Person::find($pid) ?: [];
$acc = Account::findByPerson($pid);
$avatarRaw = (string)($p['avatar'] ?? '');
Api::ok([
'person' => [
'id' => $pid,
'name' => (string)($p['name'] ?? ''),
'avatar_url' => avatar_url($avatarRaw !== '' ? $avatarRaw : null),
'role' => (string)($act['role'] ?? 'member'),
'can_write' => (bool)($act['can_write'] ?? false),
],
'account' => [
'has_login' => $acc !== null ? 1 : 0,
'role' => $acc !== null ? (string)$acc['role'] : '',
'status' => $acc !== null ? (string)$acc['status'] : '',
'last_login' => $acc !== null ? (string)($acc['last_login_at'] ?? '') : '',
// ⚠️ 密码规则由**服务端**给,端上只用来写提示语
// (避免两边各写一份"至少 8 位"然后慢慢对不上)
'password_min' => 8,
],
'device' => [
'id' => (string)($act['device_id'] ?? ''),
'name' => (string)($act['device_name'] ?? ''),
],
'is_admin' => ($act['role'] ?? '') === 'admin' ? 1 : 0,
]);
}
/**
* 按「手机号 或 用户名」找人(登录用)。
*