给所有请求都加上jwt,隐藏生成jwt的secret(放到.env中),隐藏app-secret(放在pm2运行配置文件中,后续直接读取环境配置即可)
This commit is contained in:
+98
-43
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams, Form } from "@remix-run/react";
|
||||
import { useActionData, useLoaderData, Form } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
@@ -32,11 +32,30 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
const session = await getSession(request);
|
||||
|
||||
// 读取 flash 消息(来自 callback 的错误)
|
||||
const loginError = session.get("loginError");
|
||||
|
||||
session.set("redirectTo", redirectTo);
|
||||
|
||||
// 提交 session 以清除 flash 消息
|
||||
if (loginError) {
|
||||
const { sessionStorage } = await import("~/api/login/auth.server");
|
||||
return Response.json({
|
||||
isAuthenticated: false,
|
||||
redirectTo,
|
||||
flashError: loginError
|
||||
}, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
isAuthenticated: false,
|
||||
redirectTo
|
||||
redirectTo,
|
||||
flashError: null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,10 +79,17 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 登录成功,直接返回重定向响应
|
||||
return response;
|
||||
} else {
|
||||
// 登录失败,解析错误信息并重定向到登录页面
|
||||
// 登录失败,返回错误信息(不再使用URL参数)
|
||||
const errorData = await response.json();
|
||||
const errorMsg = errorData.error || "登录失败";
|
||||
return redirect(`/login?error=${encodeURIComponent(errorMsg)}`);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: errorData.error || "登录失败",
|
||||
retryCount: errorData.retryCount || 0,
|
||||
isLocked: errorData.isLocked || false,
|
||||
remainingAttempts: errorData.remainingAttempts || 5
|
||||
}, {
|
||||
status: response.status
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,40 +97,19 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
const actionData = useActionData<typeof action>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const [isFlipped, setIsFlipped] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// 获取错误消息的友好描述
|
||||
const getErrorMessage = (error: string | null) => {
|
||||
if (!error) return null;
|
||||
|
||||
switch (error) {
|
||||
case "missing_code":
|
||||
return "登录过程中缺少授权码,请重新登录";
|
||||
case "invalid_state":
|
||||
return "登录状态验证失败,请重新登录";
|
||||
case "token_error":
|
||||
return "获取访问令牌失败,请重新登录";
|
||||
case "userinfo_error":
|
||||
return "获取用户信息失败,请重新登录";
|
||||
case "callback_error":
|
||||
return "登录回调处理失败,请重新登录";
|
||||
case "用户名和密码不能为空":
|
||||
case "用户名和密码不能为空,请重新输入":
|
||||
return "用户名和密码不能为空,请重新输入";
|
||||
case "登录失败,请检查用户名和密码":
|
||||
case "用户名或密码错误,请重新输入":
|
||||
return "用户名或密码错误,请重新输入";
|
||||
case "登录请求失败,请稍后重试":
|
||||
case "网络连接失败,请稍后重试":
|
||||
return "网络连接失败,请稍后重试";
|
||||
default:
|
||||
return decodeURIComponent(error);
|
||||
}
|
||||
};
|
||||
// 从 actionData 或 loaderData 中获取错误信息
|
||||
// actionData 的错误优先(来自密码登录)
|
||||
// loaderData.flashError 次之(来自 OAuth 回调)
|
||||
const error = actionData?.error || loaderData?.flashError;
|
||||
const isLocked = actionData?.isLocked || false;
|
||||
const retryCount = actionData?.retryCount || 0;
|
||||
const remainingAttempts = actionData?.remainingAttempts || 5;
|
||||
|
||||
// 处理OAuth2.0登录
|
||||
const handleOAuthLogin = () => {
|
||||
@@ -143,6 +148,13 @@ export default function Login() {
|
||||
|
||||
// 处理账号密码登录表单提交
|
||||
const handlePasswordLoginSubmit = (e: React.FormEvent) => {
|
||||
// 检查账户是否被锁定
|
||||
if (isLocked) {
|
||||
e.preventDefault();
|
||||
toastService.error("账户已被锁定,请联系管理员");
|
||||
return;
|
||||
}
|
||||
|
||||
// 客户端验证
|
||||
if (!username.trim()) {
|
||||
e.preventDefault();
|
||||
@@ -180,9 +192,22 @@ export default function Login() {
|
||||
<h2 className="login-subtitle">统一身份认证登录</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{getErrorMessage(error)}</div>
|
||||
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
|
||||
<div className="error-icon">
|
||||
{isLocked ? (
|
||||
<i className="ri-lock-line"></i>
|
||||
) : (
|
||||
<i className="ri-error-warning-line"></i>
|
||||
)}
|
||||
</div>
|
||||
<div className="error-text">
|
||||
{error}
|
||||
{!isLocked && retryCount > 0 && (
|
||||
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
|
||||
剩余尝试次数:{remainingAttempts} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -235,9 +260,22 @@ export default function Login() {
|
||||
<h2 className="login-subtitle">管理员登录</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{getErrorMessage(error)}</div>
|
||||
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
|
||||
<div className="error-icon">
|
||||
{isLocked ? (
|
||||
<i className="ri-lock-line"></i>
|
||||
) : (
|
||||
<i className="ri-error-warning-line"></i>
|
||||
)}
|
||||
</div>
|
||||
<div className="error-text">
|
||||
{error}
|
||||
{!isLocked && retryCount > 0 && (
|
||||
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
|
||||
剩余尝试次数:{remainingAttempts} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -275,10 +313,27 @@ export default function Login() {
|
||||
<button
|
||||
type="submit"
|
||||
className="admin-login-button"
|
||||
disabled={isLocked}
|
||||
style={isLocked ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
<i className="ri-login-box-line"></i>
|
||||
登录
|
||||
<i className={isLocked ? "ri-lock-line" : "ri-login-box-line"}></i>
|
||||
{isLocked ? "账户已锁定" : "登录"}
|
||||
</button>
|
||||
|
||||
{isLocked && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
background: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.9em',
|
||||
textAlign: 'center',
|
||||
color: '#856404'
|
||||
}}>
|
||||
<i className="ri-information-line"></i> 账户已被锁定,请联系管理员解锁
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
<div className="back-to-oauth">
|
||||
|
||||
Reference in New Issue
Block a user