115 lines
5.0 KiB
TypeScript
115 lines
5.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { adminApi, clientApi, type CaptchaResponse } from '@/api/adminApi';
|
|
import { consumeSessionRecovery, markUserActivity, readSessionRecovery, writeSession, type Portal } from '@/api/session';
|
|
import { Button, Input, Modal } from '@/components/ui';
|
|
|
|
type LoginPageProps = {
|
|
portal: Portal;
|
|
};
|
|
|
|
function loginErrorMessage(err: unknown) {
|
|
const message = err instanceof Error ? err.message : '';
|
|
if (message.includes('Invalid login or password')) return '用户名或密码错误';
|
|
if (message.includes('login and password are required')) return '请输入用户名和密码';
|
|
if (message.includes('Captcha expired')) return '验证码已过期,请刷新后重试';
|
|
if (message.includes('Captcha is incorrect')) return '验证码错误,请重新输入';
|
|
if (message.includes('User is locked')) return '账号已锁定,请 24 小时后再试';
|
|
if (message.includes('User is disabled or deleted')) return '账号已停用或已删除';
|
|
if (message.includes('Only platform admins can login to admin portal')) return '该账号不是运营端管理员';
|
|
if (message.includes('Only enterprise admins linked to a tenant can login to client portal')) return '该账号不是已绑定企业的客户端管理员';
|
|
return message || '登录失败,请检查账号信息后重试';
|
|
}
|
|
|
|
export function LoginPage({ portal }: LoginPageProps) {
|
|
const navigate = useNavigate();
|
|
const [login, setLogin] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [captchaText, setCaptchaText] = useState('');
|
|
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
|
const [error, setError] = useState('');
|
|
const [alertMessage, setAlertMessage] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const isAdmin = portal === 'admin';
|
|
const recovery = readSessionRecovery(portal);
|
|
|
|
async function refreshCaptcha(options: { clearError?: boolean } = {}) {
|
|
if (options.clearError ?? true) {
|
|
setError('');
|
|
}
|
|
setCaptchaText('');
|
|
setCaptcha(await (isAdmin ? adminApi.getCaptcha() : clientApi.getCaptcha()));
|
|
}
|
|
|
|
useEffect(() => {
|
|
void refreshCaptcha();
|
|
}, [portal]);
|
|
|
|
async function submit() {
|
|
if (!captcha) return;
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const session = await (isAdmin ? adminApi.login : clientApi.login)({
|
|
login,
|
|
password,
|
|
captchaId: captcha.captchaId,
|
|
captchaText,
|
|
});
|
|
writeSession(session);
|
|
// A login can happen without a full page reload after the previous session
|
|
// expired. Reset the in-memory activity clock so the new session is not
|
|
// immediately locked using the previous session's stale idle duration.
|
|
markUserActivity();
|
|
const target = consumeSessionRecovery(portal)?.returnUrl;
|
|
navigate(target ?? (isAdmin ? '/admin' : '/client'), { replace: true });
|
|
} catch (err) {
|
|
const message = loginErrorMessage(err);
|
|
setError(message);
|
|
setAlertMessage(message);
|
|
await refreshCaptcha({ clearError: false });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="login-page">
|
|
<section className="login-panel">
|
|
<div className="login-brand">
|
|
<img alt={isAdmin ? 'CMPP 运营端 logo' : 'CMPP 客户端 logo'} src="/logo/logo1.png" />
|
|
<div>
|
|
<h1>短信服务平台</h1>
|
|
<p>{isAdmin ? '运营端登录' : '客户端登录'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="login-form">
|
|
{recovery ? (
|
|
<p className="login-session-notice" role="status">
|
|
{recovery.message ?? '登录会话已失效,请重新登录。'} 登录成功后将返回之前访问的页面。
|
|
</p>
|
|
) : null}
|
|
<Input label="用户名/登录账号" onChange={(event) => setLogin(event.target.value)} placeholder="请输入用户名、邮箱或手机号" value={login} />
|
|
<Input label="密码" onChange={(event) => setPassword(event.target.value)} placeholder="请输入密码" type="password" value={password} />
|
|
<div className="login-captcha-row">
|
|
<Input label="图形验证码" onChange={(event) => setCaptchaText(event.target.value)} placeholder="请输入计算结果" value={captchaText} />
|
|
<button className="login-captcha" onClick={() => void refreshCaptcha()} type="button">
|
|
{captcha?.challenge ?? '刷新'}
|
|
</button>
|
|
</div>
|
|
{error ? <p className="login-error">{error}</p> : null}
|
|
<Button disabled={loading} onClick={submit}>{loading ? '登录中...' : '登录'}</Button>
|
|
</div>
|
|
</section>
|
|
<Modal
|
|
footer={<Button onClick={() => setAlertMessage('')}>知道了</Button>}
|
|
onClose={() => setAlertMessage('')}
|
|
open={Boolean(alertMessage)}
|
|
title="登录失败"
|
|
>
|
|
<p className="login-alert-message">{alertMessage}</p>
|
|
</Modal>
|
|
</main>
|
|
);
|
|
}
|