修复登录页浏览器后退卡死、已认证态下登录/注册的凭据校验绕过、系统消息分页丢失等第1-2章问题

- 最主要的一处:登录成功后点浏览器"后退",会回到一个仍然认为"已登录"的底层连接、但重新挂载出来
  的登录表单——按钮永久卡在禁用的"登录中…"(loginDisabled只有失败分支会重置,成功分支从来没有,
  因为正常路径下这个页面早就导航走了,没人会看到);如果不管这个直接切换企业码后再次提交,
  doLogin()/handleNewAccountRequest()开头的"已认证就直接放行"逻辑会让登录悄悄跳过凭据校验,
  或者让注册请求收到后端原始英文错误"already authenticated (409)"。根治方式是在
  handleHashRoute加一道门:hash指向登录/注册等纯"未登录态"视图、但连接其实已认证时,直接重定向
  离开,不让这类表单有机会被重新渲染出来——已用真实浏览器验证4个入口(login/register/tenant/
  reset/cred)全部正确重定向,且确认不影响真正的登录后安全类页面(如identity-check)。
  handleLoginSuccessful也顺手补了loginDisabled:false兜底。
- 系统消息列表:后端`GET /system-notifications`明明返回`next_cursor`表示还有更多,前端从来没用过,
  未读50+条永远只能看到最近20条、永远没法真正清零。补了"加载更多"分页——已用真实账号(60+条历史
  通知)验证,点击后正确追加下一页、游标耗尽后正确不再显示按钮。
- 已处理(通过/拒绝)的好友申请通知,原来只要event_type以"friend.request"开头就显示"查看新朋友"
  按钮——不区分"待处理"和"已是历史结果",点了跳到待处理列表也找不到对应项。改成只在
  `friend.request.created`时显示,已用真实账号里同时存在created/accepted/rejected三种通知
  验证行为符合预期。
- 注册页四处不统一的占位品牌名(讯工捷迅/捷信密使/捷讯/即时通讯,没有一个是真实品牌名)与
  系统消息页脚"即时通讯安全团队",统一改用BASE_APP_NAME("MeetingIM")。
- config.js的KNOWN_HOSTS.hosted从web.tinode.co改为web.118im.com(这是WS连接失败时的最终
  兜底地址,指向无关第三方域名不只是品牌问题,是真实会连错服务器);host-name.js的
  detectServerAddress()原来只要hostname是localhost就无条件返回硬编码的localhost:6060,
  完全不管页面实际监听的端口——本项目自己的开发代理跑在8093,清缓存后首次连接必然先撞一次
  死连接,改成和非localhost分支一样用页面自己的真实端口。
- 实名认证提交成功后,姓名/证件号输入框原来会变成空白(后端成功响应不含masked_name/
  masked_id_number,组件切到只读态时又没有回退到用户自己刚输入的草稿值)——改成没有脱敏字段时
  回退显示用户本次会话里刚输入的原始值,不是留白。
- Firebase推送通知初始化失败原来复用登录/注册表单的错误提示通道,可能盖住一个同时发生的真实
  登录错误,或在用户还没操作登录表单时就先弹出一条无关提示——推送本身是锦上添花功能,改成只
  console记录+静默关闭开关,不再抢占登录页的错误展示位。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-05 18:36:03 +08:00
co-authored by Claude Sonnet 5
parent 8b51fd3eb8
commit 47148aa75d
9 changed files with 495 additions and 399 deletions
+28 -2
View File
@@ -616,10 +616,16 @@ class TinodeWeb extends React.Component {
}
initFCMessaging() {
const {formatMessage, locale} = this.props.intl;
const {locale} = this.props.intl;
const onError = (msg, err) => {
console.error(msg, err);
this.handleError(formatMessage(messages.push_init_failed), 'err');
// Was routed through the same handleError()/errorText the login/register form's own alert
// renders from -- push notifications are best-effort (the app works fully without them), but
// sharing that channel meant this could cover up a real, concurrent login/register error, or
// (since initFCMessaging is called right as a login request starts) just surface on its own,
// with no login error at all, on a form the user hasn't touched yet. Silently degrading
// (console log + the state resets below, which already disable the desktop-alerts setting)
// is enough; failing to enable push is not something that needs to interrupt the auth flow.
this.setState({firebaseToken: null});
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
}
@@ -750,6 +756,20 @@ class TinodeWeb extends React.Component {
// Handle for hash navigation (hashchange) event: update state.
handleHashRoute() {
const hash = HashNavigation.parseUrlHash(window.location.hash);
// Browser back navigation after a successful login re-renders one of the pre-login views
// (most commonly 'login') while the underlying Tinode connection is still authenticated --
// hash routing and the WS session's lifecycle are independent, so nothing else here would
// otherwise catch this. Landing back on a login/register form while already logged in used to
// let a stale, disabled submit button sit there permanently (loginDisabled is only ever reset
// on a real login attempt's own success/failure, and none happens here), and a form actually
// submitted from this state hit doLogin's/handleNewAccountRequest's own "already authenticated"
// short-circuit -- silently skipping credential validation for login, or surfacing a raw
// untranslated "already authenticated (409)" for registration. Redirecting away before any of
// that code runs removes the only way to reach it.
if (hash.path && AUTH_FLOW_VIEWS.includes(hash.path[0]) && this.tinode.isAuthenticated()) {
HashNavigation.navigateTo('');
return;
}
// Start with panel parameters.
const newState = {
infoPanel: hash.params.info,
@@ -1073,6 +1093,12 @@ class TinodeWeb extends React.Component {
me.onSubsUpdated = this.tnMeSubsUpdated;
this.setState({
connected: true,
// Only the failure branch of doLogin's promise chain reset this; a success never needed to
// since it navigates away immediately -- until the same Login form could be reached again
// afterwards (browser back) with this still stuck true, permanently disabling the submit
// button. handleHashRoute's redirect above now closes off that specific path, but resetting
// it here too means this can't get stuck regardless of how the form ends up rendered again.
loginDisabled: false,
credMethod: undefined,
credCode: undefined,
credToken: undefined,