接入登录图形验证码流程

This commit is contained in:
andy
2026-07-15 14:16:14 +07:00
parent 99a0a483ca
commit 25da8a718e
7 changed files with 263 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
import { storeAuthTokens } from "@/constants/token";
import type { OAuthTokenResponse } from "@/constants/token";
import { getRequestContext, request, requestRaw } from "../utils/request";
import { buildSendMobileCodeRequest } from "./loginSms";
// 获取oauth token
export interface OauthTokenRequest {
@@ -13,6 +14,11 @@ export interface OauthTokenRequest {
[property: string]: any;
}
export interface SendCodeOptions {
imageRandomStr?: string;
imageCode?: string;
}
export function buildFormUrlEncodedParams(
data: Record<string, unknown>,
): URLSearchParams {
@@ -61,21 +67,18 @@ export async function oauthToken(
}
// 发送手机验证码
export function sendCode(mobile: string) {
export function sendCode(mobile: string, options: SendCodeOptions = {}) {
const value = (mobile ?? "").trim();
const encoded = encodeURIComponent(value);
const requestConfig = buildSendMobileCodeRequest({
mobile: value,
clientConfigId: getRequestContext().clientId ?? "",
imageRandomStr: options.imageRandomStr,
imageCode: options.imageCode,
});
return request(
{
url: `/admin/platformUser/sendMobileCode/${encoded}`,
method: "get",
},
{
skipAuth: true,
headers: {
clientConfigId: getRequestContext().clientId ?? "",
},
},
requestConfig.config,
requestConfig.options,
);
}

66
src/api/loginSms.test.ts Normal file
View File

@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildCaptchaImageUrl,
buildSendMobileCodeRequest,
resolveLoginErrorMessage,
} from "./loginSms.ts";
describe("login sms api helpers", () => {
it("builds send-code request with image captcha params and clientConfigId header", () => {
const result = buildSendMobileCodeRequest({
mobile: "+8613800000000",
clientConfigId: "6",
imageRandomStr: "captcha-random",
imageCode: "42",
});
assert.deepEqual(result.config, {
url: "/admin/platformUser/sendMobileCode/%2B8613800000000",
method: "get",
params: {
imageRandomStr: "captcha-random",
imageCode: "42",
},
});
assert.deepEqual(result.options, {
skipAuth: true,
headers: {
clientConfigId: "6",
},
});
});
it("builds captcha image url from api base url", () => {
assert.equal(
buildCaptchaImageUrl("captcha random", {
baseURL: "/ingress/",
cacheBust: 123,
}),
"/ingress/auth/code/image?randomStr=captcha+random&_t=123",
);
});
it("uses backend business error message before generic fallback", () => {
assert.equal(
resolveLoginErrorMessage(
{
kind: "business",
message: "图形验证码不合法",
},
"网络错误",
),
"图形验证码不合法",
);
assert.equal(
resolveLoginErrorMessage(
{
kind: "network",
message: "Network Error",
},
"网络错误",
),
"网络错误",
);
});
});

101
src/api/loginSms.ts Normal file
View File

@@ -0,0 +1,101 @@
export interface SendMobileCodeRequestParams {
mobile: string;
clientConfigId: string;
imageRandomStr?: string;
imageCode?: string;
}
export interface CaptchaImageUrlOptions {
baseURL?: string;
cacheBust?: number | string;
}
function trimEndSlash(value: string): string {
return value.replace(/\/+$/, "");
}
function resolveApiBaseURL(): string {
const env = (import.meta as ImportMeta & { env?: Record<string, unknown> }).env;
const value = env?.VITE_API_BASE_URL;
return typeof value === "string" ? value.trim() : "";
}
function appendIfPresent(
params: Record<string, string>,
key: string,
value?: string,
) {
const normalized = value?.trim();
if (normalized) {
params[key] = normalized;
}
}
export function createCaptchaRandomStr(): string {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID().replace(/-/g, "");
}
return `${Date.now()}${Math.random().toString(36).slice(2, 10)}`;
}
export function buildCaptchaImageUrl(
randomStr: string,
options: CaptchaImageUrlOptions = {},
): string {
const query = new URLSearchParams({
randomStr,
_t: String(options.cacheBust ?? Date.now()),
});
const baseURL = trimEndSlash(options.baseURL ?? resolveApiBaseURL());
return `${baseURL}/auth/code/image?${query.toString()}`;
}
export function buildSendMobileCodeRequest({
mobile,
clientConfigId,
imageRandomStr,
imageCode,
}: SendMobileCodeRequestParams) {
const params: Record<string, string> = {};
appendIfPresent(params, "imageRandomStr", imageRandomStr);
appendIfPresent(params, "imageCode", imageCode);
return {
config: {
url: `/admin/platformUser/sendMobileCode/${encodeURIComponent(
mobile.trim(),
)}`,
method: "get" as const,
params: Object.keys(params).length > 0 ? params : undefined,
},
options: {
skipAuth: true,
headers: {
clientConfigId,
},
},
};
}
export function resolveLoginErrorMessage(
error: unknown,
fallback: string,
): string {
if (!error || typeof error !== "object") {
return fallback;
}
const value = error as Record<string, unknown>;
const message = value.message;
if (
value.kind === "business" &&
typeof message === "string" &&
message.trim()
) {
return message;
}
return fallback;
}