feat(web): full auth screens rework to match design + old-app business rules

Rebuilt tenant/login/register/reset-password to match the design handoff's
01-account screens precisely (previously only loosely approximated), and
ported the old app's real business rules instead of guessing:

- LoginPage: desktop split branding panel (>=900px), terms-agreement
  checkbox gating submit, forgot-password link, password visibility toggle,
  full ctrl-error translation table (lib/tinode/errors.ts, ported from the
  old app's CTRL_ERROR_TEXT_CN/translateCtrlError -- registration/login go
  over native WS, not REST, so client.ts's REASON_MESSAGES never applied).
- RegisterPage: rebuilt as the real unified username-OR-phone form (not
  phone-only) -- username optional unless phone is blank, invite code
  required+validated only on the username-only path, matching the server's
  actual resolveRegistrationAccount rules. connection.ts's
  registerAccountByPhone generalized into registerAccount() supporting both
  paths, including the Tinode.credential(null) crash the old app had to
  work around on the username-only path.
- New ResetPasswordPage wired to /auth/password-reset/* (a different
  endpoint family than registration's /auth/verification-codes/*).
- Shared components: PasswordInput, VerificationCodeButton, Agreement,
  Toast.
- Fixed tinode-sdk.d.ts: the real method is getCurrentUserID(), not the
  invented getAuthenticatedUserID() -- caught live, login crashed past a
  successful auth.
- Fixed a responsive bug present on all four auth pages: fixed w-[380px]
  forms overflowed past the viewport edge below ~430px wide.

Verified end-to-end against the real backend (api.118im.com, tenant
"default"): fresh phone registration, password reset, re-login with the
new password, and the username-only path's invite-code rejection all
round-tripped correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-08 01:11:21 +08:00
co-authored by Claude Sonnet 5
parent 63c1b84b41
commit aa1c46ff22
12 changed files with 636 additions and 163 deletions
+70
View File
@@ -0,0 +1,70 @@
// Tinode WS {ctrl} error translation. Registration/login go over the native WS protocol, not
// REST, so client.ts's REASON_MESSAGES table (REST-only) never sees these -- the SDK's own
// CommError text is raw English with a status-code suffix baked in ("authentication failed (401)"),
// confirmed live against the real backend. Ported from the old app's tinode-web.jsx (CTRL_ERROR_TEXT_CN
// / translateCtrlError / loginErrorMessage), not guessed -- this exact table is what made login/register
// error copy match the product spec instead of leaking SDK internals.
const CTRL_ERROR_TEXT_CN: Record<string, string> = {
'authentication failed': '账号或密码错误',
'authentication required': '请先登录',
'unknown authentication scheme': '登录方式不受支持,请刷新页面重试',
'policy violation': '当前操作不符合安全策略要求',
'permission denied': '没有权限执行此操作,请重新登录',
'internal error': '服务器繁忙,请稍后重试',
};
function translateCtrlError(err: unknown): string | null {
const raw = (err as Error)?.message;
if (!raw) return null;
const normalized = raw.replace(/\s*\(\d+\)\s*$/, '').trim().toLowerCase();
return CTRL_ERROR_TEXT_CN[normalized] || null;
}
export function loginErrorMessage(err: unknown): string {
const known = translateCtrlError(err);
if (known) return known;
const code = (err as { code?: number })?.code;
if (code === 401 || code === 403) return '账号或密码错误';
return (err as Error)?.message || '登录失败,请重试';
}
export function registerErrorMessage(err: unknown): string {
const known = translateCtrlError(err);
return known || (err as Error)?.message || '注册失败,请稍后重试';
}
/**
* Only INVITE_CODE_REQUIRED/INVALID were confirmed against a live server; REVOKED/EXPIRED are
* inferred siblings under the same naming pattern (not individually exercised).
*/
const INVITE_CODE_REASON_TEXT_CN: Record<string, string> = {
INVITE_CODE_REQUIRED: '请填写邀请码',
INVITE_CODE_INVALID: '邀请码错误,请重新输入',
INVITE_CODE_REVOKED: '邀请码已被撤销,请联系管理员获取新的邀请码',
INVITE_CODE_EXPIRED: '邀请码已过期,请联系管理员获取新的邀请码',
};
export interface ValidateCredentialsParams {
reason?: string;
cred?: string[];
token?: string;
code?: string;
}
/**
* ctrl.code>=300 / ctrl.text=='validate credentials' is standard Tinode for "account created, now
* confirm a real credential (email/SMS code)" -- ctrl.params.cred is a non-empty array in that case.
* This backend reuses the exact same code/text pair for its own invite-code rejections, which carry
* `reason` instead of `cred`. This app's registration flow always pre-verifies the phone credential
* over REST before calling createAccountBasic, so a genuine pending-credential case here would mean
* an account created some other way (e.g. by an admin) -- there's no credential-confirmation screen
* built yet for that (out of scope for now), so it gets an honest, terminal message rather than a
* silent dead end.
*/
export function validateCredentialsMessage(params: ValidateCredentialsParams | undefined): string {
if (Array.isArray(params?.cred) && params.cred.length) {
return '账号存在未确认的验证信息,请联系企业管理员处理';
}
const reason = params?.reason;
return (reason && INVITE_CODE_REASON_TEXT_CN[reason]) || '注册信息校验未通过,请检查后重试';
}