feat:

添加人机校验
This commit is contained in:
john 2024-06-06 20:25:29 +08:00
parent 7b3e1bc287
commit a7a1a208dc
17 changed files with 404 additions and 118 deletions

View File

@ -1,4 +1,10 @@
###
# @LastEditors: John
# @Date: 2024-06-03 14:42:56
# @LastEditTime: 2024-06-06 17:31:31
# @Author: John
###
VITE_APP_DEV = 'dev-api' VITE_APP_DEV = 'dev-api'
# VITE_API_URL = 'http://192.168.10.166:8096' VITE_API_URL = 'http://192.168.10.166:8097'
# VITE_API_URL = 'https://8.217.122.133:10020' # VITE_API_URL = 'https://8.217.122.133:10020'
VITE_API_URL = 'https://api.pineer.cc' # VITE_API_URL = 'https://api.pineer.cc'

Binary file not shown.

View File

@ -1,7 +1,7 @@
<!-- <!--
* @LastEditors: John * @LastEditors: John
* @Date: 2024-05-22 18:06:14 * @Date: 2024-05-22 18:06:14
* @LastEditTime: 2024-05-22 18:43:14 * @LastEditTime: 2024-06-06 19:23:15
* @Author: John * @Author: John
--> -->
<!DOCTYPE html> <!DOCTYPE html>
@ -9,7 +9,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/img/logo.png" /> <link rel="icon" type="image/svg+xml" href="/src/assets/img/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>Pioneer</title> <title>Pioneer</title>
</head> </head>
<body> <body>

View File

@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@use-gesture/react": "^10.3.1",
"antd-mobile": "^5.34.0", "antd-mobile": "^5.34.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"i18next": "^23.7.20", "i18next": "^23.7.20",

View File

@ -1,6 +1,9 @@
#root { #root {
/* max-width: 1280px; */
/* margin: 0 auto; */
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
} }
.adm-center-popup-wrap {
min-width: fit-content !important;
max-width: fit-content !important;
}

View File

@ -1,7 +1,7 @@
/* /*
* @LastEditors: John * @LastEditors: John
* @Date: 2024-05-22 18:06:14 * @Date: 2024-05-22 18:06:14
* @LastEditTime: 2024-05-22 18:45:10 * @LastEditTime: 2024-06-06 16:49:46
* @Author: John * @Author: John
*/ */
import "./App.css"; import "./App.css";
@ -11,6 +11,7 @@ import SignIn from "./pages/SignIn";
import Download from "./pages/Download"; import Download from "./pages/Download";
import SelectCountry from "./pages/SelectCountry"; import SelectCountry from "./pages/SelectCountry";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Auth from "./pages/Auth";
function App() { function App() {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@ -25,6 +26,7 @@ function App() {
<Route path="/" Component={SignIn} /> <Route path="/" Component={SignIn} />
<Route path="/Download" Component={Download} /> <Route path="/Download" Component={Download} />
<Route path="/SelectCountry" Component={SelectCountry} /> <Route path="/SelectCountry" Component={SelectCountry} />
<Route path="/Auth" Component={Auth} />
</Routes> </Routes>
</div> </div>
</Router> </Router>

View File

@ -1,20 +1,41 @@
import { request } from "./request"; import { request } from "./request";
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from "axios";
export type SendCodeResType = {
sms?: boolean;
captcha?: {
bigWidth: number;
bigHeight: number;
bigImageBase64: string;
bigImage: null;
posY: number;
posX: null;
smallWidth: number;
smallHeight: number;
smallImageBase64: string;
smallImage: null;
};
};
//发送验证码 //发送验证码
export const sendCode = <T>(params: { export const sendCode = (
params: {
areaCode?: string; // 区号,只有手机号才有 areaCode?: string; // 区号,只有手机号才有
account: string; // 账号(邮箱或手机号) account: string; // 账号(邮箱或手机号)
status: 1 | 2; // 1=登录 2=注册 status: 1 | 2; // 1=登录 2=注册
signature: string; signature: string;
timestamp: string; timestamp: string;
},config:AxiosRequestConfig) => posX?: number;
request.get<T>("/api/account/sendVerificationCode", params, { },
config: AxiosRequestConfig
) =>
request.get<SendCodeResType>("/api/account/sendVerificationCode", params, {
timeout: 15000, timeout: 15000,
...config ...config,
}); });
//注册 //注册
export const signUp = <T>(params: { export const signUp = <T>(
params: {
account: string; // account account: string; // account
area?: string; // 顶级区域:朝鲜/平壤、中国/广东 area?: string; // 顶级区域:朝鲜/平壤、中国/广东
areaCode?: string; // 区号,只有手机号才有 areaCode?: string; // 区号,只有手机号才有
@ -22,8 +43,10 @@ export const signUp = <T>(params: {
district?: string; // 二级区域:具体的区域 district?: string; // 二级区域:具体的区域
shareCode: string; // invitation code shareCode: string; // invitation code
userName: string; // 用户名称 userName: string; // 用户名称
},config:AxiosRequestConfig) => },
config: AxiosRequestConfig
) =>
request.post<T>("/api/account/signUp", params, { request.post<T>("/api/account/signUp", params, {
timeout: 15000, timeout: 15000,
...config ...config,
}); });

View File

@ -32,6 +32,7 @@ service.interceptors.request.use(
config.headers = { config.headers = {
...config.headers, ...config.headers,
...customHeaders, ...customHeaders,
appVersion: "1.0.4",
} as AxiosRequestHeaders; } as AxiosRequestHeaders;
return config; return config;
@ -55,7 +56,7 @@ interface axiosTypes<T> {
interface responseTypes<T> { interface responseTypes<T> {
code: number; code: number;
msg: string; msg: string;
result: T; data: T | null;
} }
//核心处理代码 将返回一个promise 调用then将可获取响应的业务数据 //核心处理代码 将返回一个promise 调用then将可获取响应的业务数据
@ -64,7 +65,7 @@ const requestHandler = <T>(
url: string, url: string,
params: object = {}, params: object = {},
config: AxiosRequestConfig = {} config: AxiosRequestConfig = {}
): Promise<T> => { ): Promise<axiosTypes<responseTypes<T>>> => {
let response: Promise<axiosTypes<responseTypes<T>>>; let response: Promise<axiosTypes<responseTypes<T>>>;
switch (method) { switch (method) {
case "get": case "get":
@ -81,26 +82,28 @@ const requestHandler = <T>(
break; break;
} }
return new Promise<T>((resolve, reject) => { return new Promise<axiosTypes<responseTypes<T>>>((resolve, reject) => {
response response
.then((res) => { .then((res) => {
//业务代码 可根据需求自行处理 //业务代码 可根据需求自行处理
const data = res.data; // const data = res.data;
if (data.code !== 200 && data.code !== 0) { // if (data.code !== 200 && data.code !== 0) {
//特定状态码 处理特定的需求 // //特定状态码 处理特定的需求
if (data.code == 401) { // if (data.code == 401) {
console.log("登录异常,执行登出..."); // console.log("登录异常,执行登出...");
} // }
const e = JSON.stringify(data); // const e = JSON.stringify(data);
console.log(`请求错误:${e}`); // console.log(`请求错误:${e}`);
//数据请求错误 使用reject将错误返回 // //数据请求错误 使用reject将错误返回
reject(data); // reject(data);
} else { // } else {
//数据请求正确 使用resolve将结果返回 // //数据请求正确 使用resolve将结果返回
resolve(res as T); // resolve(res as axiosTypes<responseTypes<T>>);
} // }
resolve(res as axiosTypes<responseTypes<T>>);
}) })
.catch((error) => { .catch((error) => {
const e = JSON.stringify(error); const e = JSON.stringify(error);

View File

@ -1,16 +1,17 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Form, Input, Button } from "antd-mobile"; import { Form, Input, Button, Modal } from "antd-mobile";
import "../pages/SignIn.scss"; import "../pages/SignIn.scss";
import { useCountdown } from "../hooks/useCountdown"; import { useCountdown } from "../hooks/useCountdown";
import { sendCode, signUp } from "../api"; import { sendCode, signUp } from "../api";
import { sendCodeTypes, signUpTypes, ErrorType } from "../type/SignIn"; import { signUpTypes, ErrorType } from "../type/SignIn";
import { Toast } from "antd-mobile"; import { Toast } from "antd-mobile";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Md5 } from "ts-md5"; import { Md5 } from "ts-md5";
import { FormInstance } from "antd-mobile/es/components/form"; import { FormInstance } from "antd-mobile/es/components/form";
import useUserStore from "../store/user"; import useUserStore from "../store/user";
import SilderVertify, { SilderVertify_handleType } from "./SilderVertify";
function EmailForm() { function EmailForm() {
const formRef = useRef<FormInstance>(null); const formRef = useRef<FormInstance>(null);
@ -22,7 +23,7 @@ function EmailForm() {
const [shareCode, setShareCode] = useState(""); const [shareCode, setShareCode] = useState("");
const [remainingTime, setRemainingTime] = useState(0); const [remainingTime, setRemainingTime] = useState(0);
const { Lang } = useUserStore(); const { Lang } = useUserStore();
const SilderVertifyRef = useRef<SilderVertify_handleType>(null);
const config = { const config = {
headers: { headers: {
"Accept-Language": Lang, "Accept-Language": Lang,
@ -33,33 +34,59 @@ function EmailForm() {
if (email === "") { if (email === "") {
return; return;
} }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
Toast.show({ content: t("Invalid email") });
return;
}
Modal.show({
content: (
<>
<SilderVertify
account={email}
ref={SilderVertifyRef}
onFinish={async (pox) => {
const timestamp = `${new Date().getTime()}`; const timestamp = `${new Date().getTime()}`;
const res = await sendCode<sendCodeTypes>( const res = await sendCode(
{ {
account: email, account: email,
areaCode: "", areaCode: "",
status: 2, status: 2,
signature: Md5.hashStr(`Neer${email},${2},${timestamp}GetCode`), signature: Md5.hashStr(
`Neer${email},${2},${timestamp}GetCode`
),
timestamp, timestamp,
posX: pox,
}, },
config config
); );
console.log("res", res); console.log("res", res);
if (res.status === 200 && res.data.data.sms) { if (res.status === 200 && res.data.data?.sms) {
Toast.show({ Toast.show({
content: t("send successfully"), content: t("send successfully"),
afterClose: () => { afterClose: () => {
start(60 * 1000); start(60 * 1000);
}, },
}); });
Modal.clear();
} else { } else {
Toast.show({ Toast.show({
content: t("Send failure"), content: res.data.msg,
afterClose: () => { afterClose: () => {
// start(60 * 1000); // start(60 * 1000);
}, },
}); });
if (res.data.code == 1014) {
SilderVertifyRef.current?._init();
return;
} }
Modal.clear();
}
}}
/>
</>
),
closeOnMaskClick: true,
});
} catch (error: unknown) { } catch (error: unknown) {
// 检查 error 是否是 ErrorType 类型 // 检查 error 是否是 ErrorType 类型
if (typeof error === "object" && error !== null && "msg" in error) { if (typeof error === "object" && error !== null && "msg" in error) {
@ -99,8 +126,7 @@ function EmailForm() {
config config
); );
console.log("res", res); console.log("res", res);
if (res.status === 200 && res.data.data.token) { if (res.status === 200 && res.data.data?.token) {
console.log(111);
Toast.show({ Toast.show({
content: res.data.msg, content: res.data.msg,
}); });

View File

@ -1,15 +1,23 @@
/*
* @LastEditors: John
* @Date: 2024-06-03 18:43:16
* @LastEditTime: 2024-06-06 18:59:13
* @Author: John
*/
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Form, Input, Button } from "antd-mobile"; import { Form, Input, Button, Modal } from "antd-mobile";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "../pages/SignIn.scss"; import "../pages/SignIn.scss";
import useUserStore from "../store/user.ts"; import useUserStore from "../store/user.ts";
import { useCountdown } from "../hooks/useCountdown"; import { useCountdown } from "../hooks/useCountdown";
import { sendCode, signUp } from "../api"; import { sendCode, signUp } from "../api";
import { sendCodeTypes, signUpTypes, ErrorType } from "../type/SignIn"; import { signUpTypes, ErrorType } from "../type/SignIn";
import { Toast } from "antd-mobile"; import { Toast } from "antd-mobile";
import { Md5 } from "ts-md5"; import { Md5 } from "ts-md5";
import { FormInstance } from "antd-mobile/es/components/form/form"; import { FormInstance } from "antd-mobile/es/components/form/form";
import SilderVertify, { SilderVertify_handleType } from "./SilderVertify.tsx";
const defaultAreaCode = "+1";
function PhoneForm() { function PhoneForm() {
const formRef = useRef<FormInstance>(null); const formRef = useRef<FormInstance>(null);
const { t } = useTranslation(); const { t } = useTranslation();
@ -38,10 +46,10 @@ function PhoneForm() {
return ""; return "";
}; };
const defaultAreaCode = "+1";
const [authCode, setAuthCode] = useState(""); const [authCode, setAuthCode] = useState("");
const [shareCode, setShareCode] = useState(""); const [shareCode, setShareCode] = useState("");
const [remainingTime, setRemainingTime] = useState(0); const [remainingTime, setRemainingTime] = useState(0);
const SilderVertifyRef = useRef<SilderVertify_handleType>(null);
const gotoSelectCountry = () => { const gotoSelectCountry = () => {
UpdatePreviousPathName("/"); UpdatePreviousPathName("/");
@ -60,8 +68,16 @@ function PhoneForm() {
if (CurrentPhoneNumber === "") { if (CurrentPhoneNumber === "") {
return; return;
} }
Modal.show({
content: (
<>
<SilderVertify
account={CurrentPhoneNumber}
ref={SilderVertifyRef}
onFinish={async (pox) => {
const timestamp = `${new Date().getTime()}`; const timestamp = `${new Date().getTime()}`;
const res = await sendCode<sendCodeTypes>( const res = await sendCode(
{ {
account: CurrentPhoneNumber, account: CurrentPhoneNumber,
areaCode: SelectCountry?.code || defaultAreaCode, areaCode: SelectCountry?.code || defaultAreaCode,
@ -70,26 +86,37 @@ function PhoneForm() {
`Neer${CurrentPhoneNumber},${2},${timestamp}GetCode` `Neer${CurrentPhoneNumber},${2},${timestamp}GetCode`
), ),
timestamp, timestamp,
posX: pox,
}, },
config config
); );
console.log("res", res); if (res.status === 200 && res.data.data?.sms) {
if (res.status === 200 && res.data.data.sms) {
console.log(111);
Toast.show({ Toast.show({
content: t("send successfully"), content: t("send successfully"),
afterClose: () => { afterClose: () => {
start(60 * 1000); start(60 * 1000);
}, },
}); });
Modal.clear();
} else { } else {
Toast.show({ Toast.show({
content: t("Send failure"), content: res.data.msg,
afterClose: () => { afterClose: () => {
// start(60 * 1000); // start(60 * 1000);
}, },
}); });
if (res.data.code == 1014) {
SilderVertifyRef.current?._init();
return;
} }
Modal.clear();
}
}}
/>
</>
),
closeOnMaskClick: true,
});
} catch (error: unknown) { } catch (error: unknown) {
// 检查 error 是否是 ErrorType 类型 // 检查 error 是否是 ErrorType 类型
if (typeof error === "object" && error !== null && "msg" in error) { if (typeof error === "object" && error !== null && "msg" in error) {
@ -127,8 +154,7 @@ function PhoneForm() {
}, },
config config
); );
console.log("res", res); if (res.status === 200 && res.data.data?.token) {
if (res.status === 200 && res.data.data.token) {
Toast.show({ Toast.show({
content: res.data.msg, content: res.data.msg,
}); });

View File

@ -0,0 +1,148 @@
import { useDrag } from "@use-gesture/react";
import { forwardRef, useState, useImperativeHandle, useEffect } from "react";
import { Md5 } from "ts-md5";
import { sendCode } from "../api";
import useUserStore from "../store/user";
/*
* @LastEditors: John
* @Date: 2024-06-06 16:41:24
* @LastEditTime: 2024-06-06 20:25:13
* @Author: John
*/
export type SilderVertify_handleType = {
_init: () => void;
};
const SilderVertify = forwardRef<
SilderVertify_handleType,
{
account: string;
onFinish: (pox: number) => Promise<void>;
}
>(function ({ onFinish, account }, ref) {
const [offsetX, setOffsetX] = useState(0);
// Set the drag hook and define component movement based on gesture data.
const bind = useDrag(async ({ event, down, movement: [mx, _] }) => {
event.preventDefault();
// console.log(down, [mx, my]);
if (down) {
if (mx <= 0) return;
if (mx >= 280) return;
setOffsetX(mx);
} else {
await onFinish(parseInt(`${mx}`));
setOffsetX(0);
}
});
const [bigImage, setBigImage] = useState<string>("");
const [smallImage, setSmallImage] = useState<string>("");
const [smallImageY, setSmallImageY] = useState(0);
const { SelectCountry, Lang } = useUserStore();
useImperativeHandle(ref, () => {
return {
_init() {
init();
},
};
});
const config = {
headers: {
"Accept-Language": Lang,
},
};
async function init() {
setOffsetX(0);
const timestamp = `${new Date().getTime()}`;
const res = await sendCode(
{
account: account,
areaCode: SelectCountry?.code || "+1",
status: 2,
signature: Md5.hashStr(`Neer${account},${2},${timestamp}GetCode`),
timestamp,
},
config
);
if (res.status == 200 && res.data.code == 0) {
setBigImage(res.data.data?.captcha?.bigImageBase64 || "");
setSmallImage(res.data.data?.captcha?.smallImageBase64 || "");
setSmallImageY(res.data.data?.captcha?.posY || 0);
}
}
useEffect(() => {
init();
return () => {};
}, []);
return (
<div
style={{
width: "320px",
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
<span></span>
<div
style={{
width: "100%",
height: "160px",
position: "relative",
}}
>
<img
style={{ width: "100%", height: "100%" }}
src={`data:image/png;base64,${bigImage}`}
alt=""
/>
<img
style={{
width: "40px",
height: "40px",
position: "absolute",
left: `${offsetX}px`,
top: `${smallImageY}px`,
}}
src={`data:image/png;base64,${smallImage}`}
alt=""
/>
</div>
<div
style={{
width: "100%",
height: "40px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "#e4e7eb",
position: "relative",
}}
>
<div
style={{
width: "40px",
height: "40px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "#593B8B",
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "absolute",
top: 0,
left: `${offsetX}px`,
touchAction: "none",
}}
{...bind()}
>
<span>--{">"}</span>
</div>
</div>
</div>
);
});
export default SilderVertify;

View File

@ -20,5 +20,6 @@
"Download": "下載", "Download": "下載",
"send successfully": "發送成功", "send successfully": "發送成功",
"Send failure": "發送失敗", "Send failure": "發送失敗",
"Invalid phone number": "無效手機號碼" "Invalid phone number": "無效手機號碼",
"Invalid email": "無效電子郵件"
} }

View File

@ -20,5 +20,6 @@
"Download": "Download", "Download": "Download",
"send successfully": "send successfully", "send successfully": "send successfully",
"Send failure": "Send failure", "Send failure": "Send failure",
"Invalid phone number": "Invalid phone number" "Invalid phone number": "Invalid phone number",
"Invalid email": "Invalid email"
} }

15
src/pages/Auth.tsx Normal file
View File

@ -0,0 +1,15 @@
/*
* @LastEditors: John
* @Date: 2024-06-06 11:22:50
* @LastEditTime: 2024-06-06 11:35:16
* @Author: John
*/
export default function () {
return (
<>
<a href="pioneer://example/path" style={{ color: "black" }}>
Open My App with Params
</a>
</>
);
}

View File

@ -1,7 +1,16 @@
import { defineConfig } from 'vite' /*
import react from '@vitejs/plugin-react-swc' * @LastEditors: John
* @Date: 2024-05-22 18:06:14
* @LastEditTime: 2024-06-05 17:41:40
* @Author: John
*/
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
server: {
host: "192.168.10.167",
},
plugins: [react()], plugins: [react()],
}) });

View File

@ -612,6 +612,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@use-gesture/core@npm:10.3.1":
version: 10.3.1
resolution: "@use-gesture/core@npm:10.3.1"
checksum: 10c0/2e3b5c0f7fe26cdb47be3a9c2a58a6a9edafc5b2895b07d2898eda9ab5a2b29fb0098b15597baa0856907b593075cd44cc69bba4785c9cfb7b6fabaa3b52cd3e
languageName: node
linkType: hard
"@use-gesture/react@npm:10.3.0": "@use-gesture/react@npm:10.3.0":
version: 10.3.0 version: 10.3.0
resolution: "@use-gesture/react@npm:10.3.0" resolution: "@use-gesture/react@npm:10.3.0"
@ -623,6 +630,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@use-gesture/react@npm:^10.3.1":
version: 10.3.1
resolution: "@use-gesture/react@npm:10.3.1"
dependencies:
"@use-gesture/core": "npm:10.3.1"
peerDependencies:
react: ">= 16.8.0"
checksum: 10c0/978da66e4e7c424866ad52eba8fdf0ce93a4c8fc44f8837c7043e68c6a6107cd67e817fffb27f7db2ae871ef2f6addb0c8ddf1586f24c67b7e6aef1646c668cf
languageName: node
linkType: hard
"@vitejs/plugin-react-swc@npm:^3.5.0": "@vitejs/plugin-react-swc@npm:^3.5.0":
version: 3.5.0 version: 3.5.0
resolution: "@vitejs/plugin-react-swc@npm:3.5.0" resolution: "@vitejs/plugin-react-swc@npm:3.5.0"
@ -1565,6 +1583,7 @@ __metadata:
"@types/node": "npm:^20.11.7" "@types/node": "npm:^20.11.7"
"@types/react": "npm:^18.2.43" "@types/react": "npm:^18.2.43"
"@types/react-dom": "npm:^18.2.17" "@types/react-dom": "npm:^18.2.17"
"@use-gesture/react": "npm:^10.3.1"
"@vitejs/plugin-react-swc": "npm:^3.5.0" "@vitejs/plugin-react-swc": "npm:^3.5.0"
antd-mobile: "npm:^5.34.0" antd-mobile: "npm:^5.34.0"
axios: "npm:^1.6.7" axios: "npm:^1.6.7"