更新登录功能
This commit is contained in:
144
src/api/auth.ts
144
src/api/auth.ts
@@ -1,103 +1,63 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import { callByDevice, callByUser } from "./base"
|
||||
import { callPublic } from "./base"
|
||||
import { clearSession, readCookie, setSession } from "./token"
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
loginCode: string
|
||||
phone: string
|
||||
name: string
|
||||
wx: string
|
||||
qq: string
|
||||
taobao: string
|
||||
email: string
|
||||
restAmount: number
|
||||
}
|
||||
|
||||
export type LoginMode = "phone_code" | "password"
|
||||
|
||||
export async function login(props: {
|
||||
username: string
|
||||
export async function apiLogin(props: {
|
||||
logincode: string
|
||||
password: string
|
||||
remember: boolean
|
||||
mode: LoginMode
|
||||
code?: string
|
||||
wx?: string
|
||||
qq?: string
|
||||
}): Promise<ApiResponse> {
|
||||
const result = await callByDevice<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
}>("/api/auth/token", {
|
||||
...props,
|
||||
grant_type: "password",
|
||||
login_type: props.mode,
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
const result = await callPublic("/user/ApiLogin", {
|
||||
Logincode: props.logincode,
|
||||
Password: props.password,
|
||||
})
|
||||
|
||||
if (!result.success) return result
|
||||
if (!result.success) {
|
||||
return { success: false, message: result.message || "用户名或者密码不正确" }
|
||||
}
|
||||
|
||||
const { access_token, refresh_token, expires_in } = result.data
|
||||
localStorage.setItem("auth_token", access_token)
|
||||
localStorage.setItem("auth_refresh", refresh_token)
|
||||
localStorage.setItem(
|
||||
"auth_expires_in",
|
||||
String(Date.now() + expires_in * 1000),
|
||||
)
|
||||
const token = readCookie("token")
|
||||
const userInfoRaw = readCookie("userInfo")
|
||||
|
||||
return { success: true, data: undefined }
|
||||
if (!token) {
|
||||
return { success: false, message: "登录成功但未获取到 token" }
|
||||
}
|
||||
|
||||
let userInfo: unknown = userInfoRaw
|
||||
if (userInfoRaw) {
|
||||
try {
|
||||
userInfo = JSON.parse(userInfoRaw)
|
||||
} catch {
|
||||
userInfo = userInfoRaw
|
||||
}
|
||||
}
|
||||
|
||||
setSession(token, userInfo)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function apiRegister(props: {
|
||||
phone: string
|
||||
password: string
|
||||
code: string
|
||||
wx?: string
|
||||
qq?: string
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
const result = await callPublic("/user/ApiRegist", {
|
||||
Code: props.code,
|
||||
Phone: props.phone,
|
||||
Pwd: props.password,
|
||||
QQ: props.qq ?? "",
|
||||
Wx: props.wx ?? "",
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, message: result.message }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const accessToken = localStorage.getItem("auth_token")
|
||||
const refreshToken = localStorage.getItem("auth_refresh")
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
await callByUser("/api/auth/revoke", {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
localStorage.removeItem("auth_token")
|
||||
localStorage.removeItem("auth_refresh")
|
||||
localStorage.removeItem("auth_expires_in")
|
||||
|
||||
clearSession()
|
||||
window.location.href = "/"
|
||||
}
|
||||
|
||||
export async function getProfile() {
|
||||
return await callByUser<User>("/api/auth/introspect")
|
||||
}
|
||||
|
||||
export async function refreshAuth(): Promise<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
} | null> {
|
||||
const refreshToken = localStorage.getItem("auth_refresh")
|
||||
if (!refreshToken) return null
|
||||
|
||||
const resp = await callByDevice<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
}>(`/api/auth/token`, {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
localStorage.removeItem("auth_refresh")
|
||||
return null
|
||||
}
|
||||
|
||||
const { access_token, refresh_token, expires_in } = resp.data
|
||||
localStorage.setItem("auth_token", access_token)
|
||||
localStorage.setItem("auth_refresh", refresh_token)
|
||||
localStorage.setItem(
|
||||
"auth_expires_in",
|
||||
String(Date.now() + expires_in * 1000),
|
||||
)
|
||||
|
||||
return { access_token, refresh_token }
|
||||
}
|
||||
|
||||
228
src/api/base.ts
228
src/api/base.ts
@@ -3,53 +3,32 @@ import {
|
||||
type ApiResponse,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
UnauthorizedError,
|
||||
} from "@/lib/api"
|
||||
import { clearSession, getUserToken } from "./token"
|
||||
|
||||
let deviceToken: string | null = null
|
||||
let deviceTokenExpire: Date | null = null
|
||||
type CallMode = "public" | "device" | "user"
|
||||
|
||||
async function call<T = undefined>(
|
||||
async function request<R = undefined>(
|
||||
endpoint: string,
|
||||
body?: string,
|
||||
auth?: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
init: {
|
||||
method: "GET" | "POST"
|
||||
body?: string
|
||||
auth?: string
|
||||
},
|
||||
): Promise<ApiResponse<R>> {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
const headers: HeadersInit = {}
|
||||
if (init.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json"
|
||||
}
|
||||
if (auth) headers["Authorization"] = auth
|
||||
if (init.auth) headers.Authorization = init.auth
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: "POST",
|
||||
method: init.method,
|
||||
headers,
|
||||
body,
|
||||
body: init.body,
|
||||
})
|
||||
|
||||
if (response.status === 401) {
|
||||
// 如果是 刷新Token 的接口本身报 401,说明 RefreshToken 彻底失效,直接抛出错误
|
||||
if (
|
||||
endpoint.includes("/auth/token") ||
|
||||
endpoint.includes("/auth/revoke")
|
||||
) {
|
||||
throw UnauthorizedError
|
||||
}
|
||||
|
||||
// 动态引入 auth.ts 里的刷新函数,避免循环引用报错
|
||||
const { refreshAuth } = await import("./auth")
|
||||
|
||||
// 尝试刷新 Token
|
||||
const newTokens = await refreshAuth()
|
||||
|
||||
if (newTokens) {
|
||||
// 刷新成功,用新 Token 重试当前请求(递归调用)
|
||||
return call(endpoint, body, `Bearer ${newTokens.access_token}`)
|
||||
} else {
|
||||
// 刷新失败,抛出 401 错误,让上层处理跳转
|
||||
throw UnauthorizedError
|
||||
}
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("Content-Type") ?? "text/plain"
|
||||
|
||||
if (contentType.includes("text/plain")) {
|
||||
@@ -61,11 +40,18 @@ async function call<T = undefined>(
|
||||
message: text || "请求失败",
|
||||
}
|
||||
}
|
||||
return { success: true, data: undefined as T }
|
||||
return { success: true, data: undefined as R }
|
||||
}
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
const json = await response.json()
|
||||
const json = (await response.json()) as R & {
|
||||
message?: string
|
||||
error_description?: string
|
||||
Code?: number
|
||||
Message?: string
|
||||
Data?: unknown
|
||||
data?: unknown
|
||||
}
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -73,6 +59,20 @@ async function call<T = undefined>(
|
||||
message: json.message || json.error_description || "请求失败",
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof json.Code === "number") {
|
||||
if (json.Code === 10000) {
|
||||
return {
|
||||
success: true,
|
||||
data: (json.Data ?? json.data ?? undefined) as R,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: json.Message || "请求失败",
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: json }
|
||||
}
|
||||
|
||||
@@ -87,72 +87,124 @@ async function call<T = undefined>(
|
||||
}
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Public
|
||||
// ======================
|
||||
let deviceToken: string | null = null
|
||||
let deviceTokenExpire = 0
|
||||
let deviceTokenPromise: Promise<string | null> | null = null
|
||||
|
||||
async function acquireDeviceToken(): Promise<string | null> {
|
||||
if (deviceToken && Date.now() < deviceTokenExpire) return deviceToken
|
||||
if (!deviceTokenPromise) {
|
||||
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
|
||||
deviceTokenPromise = request<{
|
||||
access_token: string
|
||||
expires_in: number
|
||||
}>("/api/auth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ grant_type: "client_credentials" }),
|
||||
auth: `Basic ${basic}`,
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.success) {
|
||||
deviceToken = null
|
||||
return null
|
||||
}
|
||||
deviceToken = resp.data.access_token
|
||||
deviceTokenExpire = Date.now() + (resp.data.expires_in - 60) * 1000
|
||||
return resp.data.access_token
|
||||
})
|
||||
.finally(() => {
|
||||
deviceTokenPromise = null
|
||||
})
|
||||
}
|
||||
return deviceTokenPromise
|
||||
}
|
||||
|
||||
const inflight = new Map<string, Promise<ApiResponse<unknown>>>()
|
||||
|
||||
function dedupGet<R>(
|
||||
endpoint: string,
|
||||
run: () => Promise<ApiResponse<R>>,
|
||||
): Promise<ApiResponse<R>> {
|
||||
const existing = inflight.get(endpoint)
|
||||
if (existing) return existing as Promise<ApiResponse<R>>
|
||||
const promise = run().finally(() => inflight.delete(endpoint))
|
||||
inflight.set(endpoint, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
async function call<R = undefined>(
|
||||
mode: CallMode,
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
const body = data === undefined ? undefined : JSON.stringify(data)
|
||||
|
||||
const run = async (): Promise<ApiResponse<R>> => {
|
||||
let auth: string | undefined
|
||||
if (mode === "user") {
|
||||
const token = getUserToken()
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
message: "未登录或会话已过期",
|
||||
}
|
||||
}
|
||||
auth = `Bearer ${token}`
|
||||
} else if (mode === "device") {
|
||||
const token = await acquireDeviceToken()
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
message: "设备凭证获取失败",
|
||||
}
|
||||
}
|
||||
auth = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const resp = await request<R>(endpoint, { method, body, auth })
|
||||
|
||||
if (!resp.success && resp.status === 401) {
|
||||
if (mode === "user") {
|
||||
clearSession()
|
||||
} else if (mode === "device") {
|
||||
deviceToken = null
|
||||
deviceTokenExpire = 0
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
if (method === "GET" && data === undefined) {
|
||||
return dedupGet<R>(endpoint, run)
|
||||
}
|
||||
return run()
|
||||
}
|
||||
|
||||
async function callPublic<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
return call(endpoint, data ? JSON.stringify(data) : undefined)
|
||||
return call<R>("public", endpoint, data, method)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Device
|
||||
// ======================
|
||||
async function callByDevice<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
// 检查内存中的 Token 是否过期
|
||||
if (!deviceToken || !deviceTokenExpire || new Date() >= deviceTokenExpire) {
|
||||
// 用 btoa 生成 Basic 认证头
|
||||
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)
|
||||
const resp = await call<{
|
||||
access_token: string
|
||||
expires_in: number
|
||||
}>(
|
||||
"/api/auth/token",
|
||||
JSON.stringify({ grant_type: "client_credentials" }),
|
||||
`Basic ${basic}`,
|
||||
)
|
||||
|
||||
if (!resp.success) {
|
||||
return resp
|
||||
}
|
||||
deviceToken = resp.data.access_token
|
||||
// 提前 60 秒过期,防止临界点失效
|
||||
deviceTokenExpire = new Date(
|
||||
Date.now() + (resp.data.expires_in - 60) * 1000,
|
||||
)
|
||||
}
|
||||
|
||||
return call(
|
||||
endpoint,
|
||||
data ? JSON.stringify(data) : undefined,
|
||||
`Bearer ${deviceToken}`,
|
||||
)
|
||||
return call<R>("device", endpoint, data, method)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// User
|
||||
// ======================
|
||||
async function callByUser<R = undefined>(
|
||||
endpoint: string,
|
||||
data?: unknown,
|
||||
method: "GET" | "POST" = "POST",
|
||||
): Promise<ApiResponse<R>> {
|
||||
// 这里我们假设用户登录后的 Token 存在 localStorage 里
|
||||
// 如果你是用 Cookie 存 Token,这里用 js-cookie 的 get 方法替换即可
|
||||
const token = localStorage.getItem("auth_token")
|
||||
|
||||
if (!token) {
|
||||
throw UnauthorizedError
|
||||
}
|
||||
return call(
|
||||
endpoint,
|
||||
data ? JSON.stringify(data) : undefined,
|
||||
`Bearer ${token}`,
|
||||
)
|
||||
return call<R>("user", endpoint, data, method)
|
||||
}
|
||||
|
||||
export { callByDevice, callByUser, callPublic }
|
||||
|
||||
9
src/api/captcha.ts
Normal file
9
src/api/captcha.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { callPublic } from "./base"
|
||||
|
||||
export async function getCaptcha() {
|
||||
return await callPublic<{ token: string; base64: string }>("/api/captcha/get")
|
||||
}
|
||||
|
||||
export async function verifyCaptcha(token: string, answer: string) {
|
||||
return await callPublic<boolean>("/api/captcha/verify", { token, answer })
|
||||
}
|
||||
64
src/api/order.ts
Normal file
64
src/api/order.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import { callByUser } from "./base"
|
||||
|
||||
export type CreateOrderRequest = {
|
||||
PackageId: number
|
||||
OrderType: number
|
||||
Account: string
|
||||
Pwd: string
|
||||
ConnectCount: number
|
||||
MinPostfix?: number
|
||||
MaxPostfix?: number
|
||||
CouponId?: number
|
||||
UseAccountAmount?: number
|
||||
OPayType: number
|
||||
PayChannel?: number
|
||||
Price?: number
|
||||
}
|
||||
|
||||
export type CreateTestAccountRequest = {
|
||||
ProductId: number
|
||||
PackageId: number
|
||||
Account: string
|
||||
Pwd: string
|
||||
}
|
||||
|
||||
export type OrderInfo = {
|
||||
OrderNo: string
|
||||
OtherPayAmount: number
|
||||
CreateTime: string
|
||||
PayType: number
|
||||
}
|
||||
|
||||
export type CreateOrderData = {
|
||||
OrderInfo: OrderInfo
|
||||
PayData: string
|
||||
}
|
||||
|
||||
export async function createOrder(
|
||||
data: CreateOrderRequest,
|
||||
): Promise<ApiResponse<CreateOrderData | string>> {
|
||||
return await callByUser<CreateOrderData | string>(
|
||||
"/product/CreateOrder",
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
export async function createTestAccount(
|
||||
data: CreateTestAccountRequest,
|
||||
): Promise<ApiResponse<string>> {
|
||||
return await callByUser<string>(
|
||||
"/api/course/v1/productaccount/CreateTestAccount",
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkPayStatus(
|
||||
orderNo: string,
|
||||
): Promise<ApiResponse<number>> {
|
||||
return await callByUser<number>(
|
||||
`/product/IsPay?orderNo=${orderNo}`,
|
||||
undefined,
|
||||
"GET",
|
||||
)
|
||||
}
|
||||
11
src/api/product.ts
Normal file
11
src/api/product.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type { ProductItem } from "@/lib/models/product"
|
||||
import { callPublic } from "./base"
|
||||
|
||||
export async function fetchProducts(): Promise<ApiResponse<ProductItem[]>> {
|
||||
return await callPublic<ProductItem[]>(
|
||||
"/product/ApiProductActive",
|
||||
undefined,
|
||||
"GET",
|
||||
)
|
||||
}
|
||||
39
src/api/token.ts
Normal file
39
src/api/token.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
const AUTH_EVENT = "auth-change"
|
||||
|
||||
const TOKEN_KEY = "auth_token"
|
||||
const USER_KEY = "auth_user"
|
||||
|
||||
export function readCookie(name: string): string | null {
|
||||
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
export function getUserToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function getUserInfo<T = unknown>(): T | null {
|
||||
const raw = localStorage.getItem(USER_KEY)
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw) as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isAuthed(): boolean {
|
||||
return getUserToken() !== null
|
||||
}
|
||||
|
||||
export function setSession(token: string, userInfo: unknown) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(userInfo))
|
||||
window.dispatchEvent(new Event(AUTH_EVENT))
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
window.dispatchEvent(new Event(AUTH_EVENT))
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import { callByDevice } from "./base"
|
||||
import { callPublic } from "./base"
|
||||
|
||||
export type SMSPurpose = "User_Code" | "FindUser_Code" | "User_Login"
|
||||
|
||||
export async function sendSMS(
|
||||
phone: string,
|
||||
purpose: SMSPurpose = "User_Code",
|
||||
): Promise<ApiResponse> {
|
||||
return await callByDevice("/api/verify/sms", {
|
||||
phone,
|
||||
purpose,
|
||||
): Promise<ApiResponse<undefined>> {
|
||||
return await callPublic("/user/SendPhonesCodevefy", {
|
||||
key: purpose,
|
||||
phone,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user