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:
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按「手机号 或 用户名」找人(登录用)。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user