🌱 验证码登录请求草创 #118

This commit is contained in:
目棃
2024-07-25 15:45:26 +08:00
parent db3cb2fa29
commit 20ad79f08b
8 changed files with 1271 additions and 5 deletions

View File

@@ -81,6 +81,7 @@
"clipboard": "^2.0.11",
"color-convert": "^2.0.1",
"echarts": "^5.5.1",
"geetest": "^4.1.2",
"html2canvas": "^1.4.1",
"js-md5": "^0.8.3",
"pinia": "^2.1.7",
@@ -132,6 +133,7 @@
"typescript": "^5.5.3",
"typescript-eslint": "^7.16.1",
"vite": "^5.3.4",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-vue-devtools": "^7.3.6",
"vite-plugin-vuetify": "^2.0.3",
"vue-eslint-parser": "^9.4.3",

979
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
/**
* @file plugins/Mys/index.ts
* @description Mys plugin index
* @since Beta v0.4.5
* @since Beta v0.5.1
*/
import MysApi from "./api/index.js";
import { getCaptcha, doCaptchaLogin } from "./request/doCaptchaLogin.js";
import { getLoginQr, getLoginStatus } from "./request/doGameLogin.js";
import { getCollectionData, getCollectionPosts } from "./request/getCollectionData.js";
import getForumList from "./request/getForumList.js";
@@ -50,6 +51,8 @@ const Mys = {
User: {
getQr: getLoginQr,
getData: getLoginStatus,
getCaptcha,
login: doCaptchaLogin,
},
Vote: {
get: getVoteInfo,

View File

@@ -0,0 +1,112 @@
/**
* @file plugins/Mys/request/doCaptchaLogin.ts
* @description 通过短信验证码登录账号获取 stoken
* @since Beta v0.5.1
*/
import { publicEncrypt } from "node:crypto";
import TGHttp from "../../../utils/TGHttp.js";
import { getDeviceInfo } from "../../../utils/toolFunc.js";
import TGConstant from "../../../web/constant/TGConstant.js";
const PUB_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDvekdPMHN3AYhm/vktJT+YJr7cI5DcsNK
qdsx5DZX0gDuWFuIjzdwButrIYPNmRJ1G8ybDIF7oDW2eEpm5sMbL9zs9ExXCdvqrn51qELb
qj0XxtMTIpaCHFSI50PfPpTFV9Xt/hmyVwokoOXFlAEgCn+QCgGs52bFoYMtyi+xEQIDAQAB
-----END PUBLIC KEY-----`;
const EncryptAreaCode = rsaEncrypt("+86");
/**
* @description rsa 加密
* @since Beta v0.5.1
* @param {string} data - 待加密数据
* @returns {string} 加密后数据
*/
function rsaEncrypt(data: string): string {
const buffer = Buffer.from(data);
return publicEncrypt(PUB_KEY, buffer).toString("base64");
}
/**
* @description 获取短信验证码
* @since Beta v0.5.1
* @todo retcode 为-3101时表示需要进行验证需要从resp.headers["x-rpc-aigis"]中获取相关数据
* @param {string} phone - 手机号
* @returns {Promise<TGApp.Plugins.Mys.CaptchaLogin.CaptchaData | TGApp.BBS.Response.Base>}
*/
export async function getCaptcha(
phone: string,
): Promise<TGApp.Plugins.Mys.CaptchaLogin.CaptchaData | TGApp.BBS.Response.Base> {
const url = "https://passport-api.mihoyo.com/account/ma-cn-verifier/verifier/createLoginCaptcha";
const device_fp = getDeviceInfo("device_fp");
const device_name = getDeviceInfo("device_name");
const device_id = getDeviceInfo("device_id");
const device_model = getDeviceInfo("product");
const body = { area: EncryptAreaCode, phone: rsaEncrypt(phone) };
const header = {
"x-rpc-aigis": "",
"x-rpc-app_version": TGConstant.BBS.VERSION,
"x-rpc-client_type": "2",
"x-rpc-app_id": TGConstant.BBS.APP_ID,
"x-rpc-device_fp": device_fp,
"x-rpc-device_name": device_name,
"x-rpc-device_id": device_id,
"x-rpc-device_model": device_model,
};
const resp = await TGHttp<
TGApp.Plugins.Mys.CaptchaLogin.CaptchaResponse | TGApp.BBS.Response.Base
>(url, {
method: "POST",
headers: header,
body: JSON.stringify(body),
});
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp.data;
}
/**
* @description 通过短信验证码登录
* @since Beta v0.5.1
* @param {string} phone - 手机号
* @param {string} action_type - 操作类型
* @param {string} captcha - 验证码
* @returns {Promise<TGApp.Plugins.Mys.CaptchaLogin.LoginData | TGApp.BBS.Response.Base>}
*/
export async function doCaptchaLogin(
phone: string,
action_type: string,
captcha: string,
): Promise<TGApp.Plugins.Mys.CaptchaLogin.LoginData | TGApp.BBS.Response.Base> {
const url = "https://passport-api.mihoyo.com/account/ma-cn-passport/app/loginByMobileCaptcha";
const device_fp = getDeviceInfo("device_fp");
const device_name = getDeviceInfo("device_name");
const device_id = getDeviceInfo("device_id");
const device_model = getDeviceInfo("product");
const body = {
area: EncryptAreaCode,
phone: rsaEncrypt(phone),
action_type,
captcha,
};
const header = {
"x-rpc-aigis": "",
"x-rpc-app_version": TGConstant.BBS.VERSION,
"x-rpc-client_type": "2",
"x-rpc-app_id": TGConstant.BBS.APP_ID,
"x-rpc-device_fp": device_fp,
"x-rpc-device_name": device_name,
"x-rpc-device_id": device_id,
"x-rpc-device_model": device_model,
};
const resp = await TGHttp<TGApp.Plugins.Mys.CaptchaLogin.LoginResponse | TGApp.BBS.Response.Base>(
url,
{
method: "POST",
headers: header,
body: JSON.stringify(body),
},
);
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
return resp.data;
}

163
src/plugins/Mys/types/CaptchaLogin.d.ts vendored Normal file
View File

@@ -0,0 +1,163 @@
/**
* @file plugins/Mys/types/CaptchaLogin.d.ts
* @description Mys 插件 Captcha 登录类型定义文件
* @since Beta v0.5.1
*/
/**
* @description Mys 插件 Captcha 登录类型
* @since Beta v0.5.1
* @namespace TGApp.Plugins.Mys.CaptchaLogin
* @memberof TGApp.Plugins.Mys
*/
declare namespace TGApp.Plugins.Mys.CaptchaLogin {
/**
* @description 获取短信验证码返回数据
* @since Beta v0.5.1
* @interface CaptchaResponse
* @extends TGApp.BBS.Response.BaseWithData
* @property {CaptchaData} data 数据
* @return GetCaptchaResponse
*/
interface CaptchaResponse extends TGApp.BBS.Response.BaseWithData {
data: CaptchaData;
}
/**
* @description 获取短信验证码返回数据
* @since Beta v0.5.1
* @interface CaptchaData
* @property {string} sent_new 是否发送新验证码
* @property {string} countdown 倒计时
* @property {string} action_type 操作类型
* @return CaptchaData
*/
interface CaptchaData {
sent_new: string;
countdown: string;
action_type: string;
}
/**
* @description 短信验证码登录返回数据
* @since Beta v0.5.1
* @interface LoginResponse
* @extends TGApp.BBS.Response.BaseWithData
* @property {LoginData} data 数据
* @return LoginResponse
*/
interface LoginResponse extends TGApp.BBS.Response.BaseWithData {
data: LoginData;
}
/**
* @description 短信验证码登录返回数据
* @since Beta v0.5.1
* @interface LoginData
* @property {Token} token token数据
* @property {UserInfo} user_info 用户信息
* @property {ReactivateInfo} reactivate_info 重新激活信息
* @property {string} login_ticket 登录 ticket
* @property {boolean} new_user 是否新用户
* @property {RealnameInfo} realname_info 实名信息
* @property {boolean} need_realperson 是否需要实名认证
* @property {string} oauth_hw_open_id 华为 open id
* @return LoginData
*/
interface LoginData {
token: Token;
user_info: UserInfo;
reactivate_info: ReactivateInfo;
login_ticket: string;
new_user: boolean;
realname_info: RealnameInfo;
need_realperson: boolean;
oauth_hw_open_id: string;
}
/**
* @description token 数据
* @since Beta v0.5.1
* @interface Token
* @property {number} token_type token 类型
* @property {string} token token
* @return Token
*/
interface Token {
token_type: number;
token: string;
}
/**
* @description 用户信息
* @since Beta v0.5.1
* @interface UserInfo
* @property {string} aid 账号 id
* @property {string} mid mid
* @property {string} account_name 账号名称
* @property {string} email 邮箱
* @property {number} is_email_verify 是否验证邮箱
* @property {string} area_code 手机区号
* @property {string} mobile 手机号
* @property {string} safe_area_code 安全手机区号
* @property {string} safe_mobile 安全手机号
* @property {string} realname 真实姓名
* @property {string} identity_code 身份证号
* @property {string} rebind_area_code 重新绑定手机区号
* @property {string} rebind_mobile 重新绑定手机号
* @property {string} rebind_mobile_time 重新绑定手机时间
* @property {string[]} links 账号绑定信息
* @property {string} country 国家
* @property {string} unmasked_email 邮箱
* @property {number} unmasked_email_type 邮箱类型
* @return UserInfo
*/
interface UserInfo {
aid: string;
mid: string;
account_name: string;
email: string;
is_email_verify: number;
area_code: string;
mobile: string;
safe_area_code: string;
safe_mobile: string;
realname: string;
identity_code: string;
rebind_area_code: string;
rebind_mobile: string;
rebind_mobile_time: string;
links: string[];
country: string;
unmasked_email: string;
unmasked_email_type: number;
}
/**
* @description 重新激活信息
* @since Beta v0.5.1
* @interface ReactivateInfo
* @property {boolean} required 是否需要重新激活
* @property {string} ticket 重新激活 ticket
* @return ReactivateInfo
*/
interface ReactivateInfo {
required: boolean;
ticket: string;
}
/**
* @description 实名信息
* @since Beta v0.5.1
* @interface RealnameInfo
* @property {boolean} required 是否需要实名认证
* @property {string} action_type 操作类型
* @property {string} action_ticket 操作 ticket
* @return RealnameInfo
*/
interface RealnameInfo {
required: boolean;
action_type: string;
action_ticket: string;
}
}

View File

@@ -81,7 +81,7 @@ export function getInitDeviceInfo(): TGApp.App.Device.DeviceInfo {
* @param {string} key - 设备信息 key
* @returns {string} 设备信息
*/
export function getDeviceInfo(key: "device_id" | "device_fp"): string {
export function getDeviceInfo(key: keyof TGApp.App.Device.DeviceInfo): string {
const localDevice = localStorage.getItem("deviceInfo");
let deviceInfo: TGApp.App.Device.DeviceInfo;
if (localDevice === null) {

View File

@@ -11,7 +11,13 @@
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["DOM", "ESNext"],
"types": ["vite/client"],
"types": [
"vite/client",
"src/types",
"src/plugins/Bili/types",
"src/plugins/Hutao/types",
"src/plugins/Mys/types"
],
"allowSyntheticDefaultImports": true,
"composite": true
},

View File

@@ -1,11 +1,12 @@
/**
* @file vite.config.ts
* @description vite 配置文件
* @since Beta v0.5.0
* @since Beta v0.5.1
*/
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import VueDevtools from "vite-plugin-vue-devtools";
import vuetify from "vite-plugin-vuetify";
@@ -13,7 +14,7 @@ import buildTimePlugin from "./src/utils/TGBuild.js";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vuetify(), buildTimePlugin(), VueDevtools()],
plugins: [vue(), vuetify(), buildTimePlugin(), VueDevtools(), nodePolyfills()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors