群主转让加一步密码二次确认,跟账号注销/改密码等其它高风险操作保持一致(第6章复测发现)
serveIMGroupOwnerTransfer 核实过:只认 {target_user_id, version},没有任何
action_token/二次验证机制——不是文档滞后,是这个操作本身就没有服务端强制的二次
确认。但转让群主同样不可逆、后果同样重大("确定后你将立即失去群主身份"),本项目
对同一类场景已有明确先例:personal-settings-view.jsx 的 DeleteAccountConfirm 就是
纯前端加的密码输入门槛,不指望服务端校验它,只是让"手滑点确定"变成需要多打一遍
密码才会发生的事。
OwnerSelect 原来点目标人选后只有一层"确定/取消"的确认表,点确定直接调用真实转让
接口。现在拆成两步:第一步维持原有确认文案不变,第二步新增密码输入,留空点确定
会被拦下(不发请求),填了任意内容才真正调用 transferGroupOwner。
真实验证(真实群+真实候选人):空密码点确定确认没有发出请求;填入密码后
transferGroupOwner 真实调用成功,正确跳转回群组管理页。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
542aca3dc2
commit
7728afe79f
@@ -9513,6 +9513,17 @@ div.toast.show {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.group-password-input {
|
||||
width: 100%;
|
||||
height: 2.75rem;
|
||||
margin-top: 0.6rem;
|
||||
padding: 0 0.9rem;
|
||||
border: 1px solid #dde1e6;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.people-confirm-sheet button,
|
||||
.group-source-sheet button,
|
||||
.group-confirm-sheet button {
|
||||
|
||||
@@ -831,6 +831,13 @@ function OwnerSelect({topic, onToast}) {
|
||||
const [members, setMembers] = React.useState(null);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [target, setTarget] = React.useState(null);
|
||||
// 后端 serveIMGroupOwnerTransfer 只认 {target_user_id, version},没有任何二次验证/action_token
|
||||
// 机制(跟青少年模式关闭那种真的会被服务端拒绝的场景不一样)。群主转让同样不可逆、后果同样
|
||||
// 重大,照抄本项目已有的处理这类"后端没有二次验证但操作本身很危险"场景的方式——见
|
||||
// personal-settings-view.jsx 的 DeleteAccountConfirm:加一步前端自己的密码输入门槛,不追加到
|
||||
// 请求体、不指望服务端校验它,只是让"手滑点确定"变成需要多一步真实操作才会发生的事。
|
||||
const [passwordStep, setPasswordStep] = React.useState(false);
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [loadError, setLoadError] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
@@ -841,7 +848,9 @@ function OwnerSelect({topic, onToast}) {
|
||||
const visible = (members || []).filter(m => (m.group_nickname || m.display_name || '').includes(query));
|
||||
const admins = visible.filter(m => m.role === 'admin');
|
||||
const others = visible.filter(m => m.role !== 'admin');
|
||||
const reset = () => { setTarget(null); setPasswordStep(false); setPassword(''); };
|
||||
const confirm = () => {
|
||||
if (!password) { onToast('请输入密码以确认'); return; }
|
||||
if (busy || !target) return;
|
||||
setBusy(true);
|
||||
transferGroupOwner(topic, target.user_id).then(() => { onToast('群主已转让'); navigate('group-governance', topic); })
|
||||
@@ -859,11 +868,19 @@ function OwnerSelect({topic, onToast}) {
|
||||
{others.length ? <><div className="group-section-title">群成员</div>{others.map(row)}</> : null}
|
||||
</>}
|
||||
</main>
|
||||
{target ? <div className="group-sheet-backdrop"><section className="group-confirm-sheet">
|
||||
{target && !passwordStep ? <div className="group-sheet-backdrop"><section className="group-confirm-sheet">
|
||||
<h2>选择新群主</h2>
|
||||
<p>{target.group_nickname || target.display_name}成为新群主,确定后你将立即失去群主身份</p>
|
||||
<button type="button" className="danger" disabled={busy} onClick={confirm}>确定</button>
|
||||
<button type="button" onClick={() => setTarget(null)}>取消</button>
|
||||
<button type="button" className="danger" onClick={() => setPasswordStep(true)}>确定</button>
|
||||
<button type="button" onClick={reset}>取消</button>
|
||||
</section></div> : null}
|
||||
{target && passwordStep ? <div className="group-sheet-backdrop"><section className="group-confirm-sheet">
|
||||
<h2>输入密码确认转让</h2>
|
||||
<p>请输入登录密码以确认将群主转让给{target.group_nickname || target.display_name}(该操作立即生效,不可撤销)。</p>
|
||||
<input type="password" autoFocus className="group-password-input" value={password}
|
||||
onChange={e => setPassword(e.target.value)} placeholder="登录密码" />
|
||||
<button type="button" className="danger" disabled={busy} onClick={confirm}>{busy ? '处理中…' : '确定'}</button>
|
||||
<button type="button" onClick={reset}>取消</button>
|
||||
</section></div> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+41
-4
@@ -12097,6 +12097,8 @@ function OwnerSelect({
|
||||
const [members, setMembers] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
|
||||
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||
const [target, setTarget] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
|
||||
const [passwordStep, setPasswordStep] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const [password, setPassword] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||
const [busy, setBusy] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
@@ -12111,7 +12113,16 @@ function OwnerSelect({
|
||||
const visible = (members || []).filter(m => (m.group_nickname || m.display_name || '').includes(query));
|
||||
const admins = visible.filter(m => m.role === 'admin');
|
||||
const others = visible.filter(m => m.role !== 'admin');
|
||||
const reset = () => {
|
||||
setTarget(null);
|
||||
setPasswordStep(false);
|
||||
setPassword('');
|
||||
};
|
||||
const confirm = () => {
|
||||
if (!password) {
|
||||
onToast('请输入密码以确认');
|
||||
return;
|
||||
}
|
||||
if (busy || !target) return;
|
||||
setBusy(true);
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.transferGroupOwner)(topic, target.user_id).then(() => {
|
||||
@@ -12159,7 +12170,7 @@ function OwnerSelect({
|
||||
}, void 0, false), others.map(row)]
|
||||
}, void 0, true) : null]
|
||||
}, void 0, true)
|
||||
}, void 0, false), target ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||
}, void 0, false), target && !passwordStep ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||
className: "group-sheet-backdrop",
|
||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("section", {
|
||||
className: "group-confirm-sheet",
|
||||
@@ -12170,12 +12181,38 @@ function OwnerSelect({
|
||||
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
className: "danger",
|
||||
disabled: busy,
|
||||
onClick: confirm,
|
||||
onClick: () => setPasswordStep(true),
|
||||
children: "确定"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
onClick: () => setTarget(null),
|
||||
onClick: reset,
|
||||
children: "取消"
|
||||
}, void 0, false)]
|
||||
}, void 0, true)
|
||||
}, void 0, false) : null, target && passwordStep ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||
className: "group-sheet-backdrop",
|
||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("section", {
|
||||
className: "group-confirm-sheet",
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("h2", {
|
||||
children: "输入密码确认转让"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
|
||||
children: ["请输入登录密码以确认将群主转让给", target.group_nickname || target.display_name, "(该操作立即生效,不可撤销)。"]
|
||||
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("input", {
|
||||
type: "password",
|
||||
autoFocus: true,
|
||||
className: "group-password-input",
|
||||
value: password,
|
||||
onChange: e => setPassword(e.target.value),
|
||||
placeholder: "登录密码"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
className: "danger",
|
||||
disabled: busy,
|
||||
onClick: confirm,
|
||||
children: busy ? '处理中…' : '确定'
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
onClick: reset,
|
||||
children: "取消"
|
||||
}, void 0, false)]
|
||||
}, void 0, true)
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user