th-app-auth.ts
5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { unwrapAbpResponse } from '#/api/th/abp-unwrap';
/** 绑定门店(与 ThAppLoginOutputDto.locations 一致) */
export interface ThAppBoundLocationDto {
id: string;
locationCode: string;
locationName: string;
fullAddress: string;
state: boolean;
}
export interface ThAppLoginInput {
tenantId: string;
email: string;
password: string;
uuid?: string;
code?: string;
}
export interface ThAppLoginOutputDto {
token: string;
refreshToken: string;
tenantId: string;
tenantName: string;
locations: ThAppBoundLocationDto[];
}
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
/** 防止重复提交导致前一个请求被 Abort、后一个已在 Network 成功但 UI 仍报错 */
let loginInFlight: Promise<ThAppLoginOutputDto> | null = null;
function getApiBase(): string {
const raw = (import.meta.env.VITE_GLOB_API_URL as string | undefined) ?? '/dev-api';
return raw.replace(/^"|"$/g, '').replace(/\/$/, '');
}
function normalizeLocation(raw: Record<string, unknown>): ThAppBoundLocationDto {
return {
id: String(raw.id ?? raw.Id ?? ''),
locationCode: String(raw.locationCode ?? raw.LocationCode ?? ''),
locationName: String(raw.locationName ?? raw.LocationName ?? ''),
fullAddress: String(raw.fullAddress ?? raw.FullAddress ?? ''),
state: raw.state !== false && raw.State !== false,
};
}
function normalizeLoginOutput(raw: unknown): ThAppLoginOutputDto {
const o = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
const locs = o.locations ?? o.Locations;
const arr = Array.isArray(locs) ? locs : [];
return {
token: String(o.token ?? o.Token ?? ''),
refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''),
tenantId: String(o.tenantId ?? o.TenantId ?? ''),
tenantName: String(o.tenantName ?? o.TenantName ?? ''),
locations: arr.map((x) =>
normalizeLocation(x as Record<string, unknown>),
),
};
}
function normalizeLocationList(raw: unknown): ThAppBoundLocationDto[] {
const arr = Array.isArray(raw) ? raw : [];
return arr.map((x) => normalizeLocation(x as Record<string, unknown>));
}
/**
* 使用 XHR 登录:dev 代理下 fetch/axios 常出现「Network 已有 body 但 JS 一直等连接结束」。
* XHR onload 在收齐 responseText 后即触发,不依赖 fetch 的 body 流结束。
*/
function thAppLoginXhr(input: ThAppLoginInput): Promise<ThAppLoginOutputDto> {
const url = `${getApiBase()}/th-app-auth/login`;
const language = preferences.app.locale.replace('-', '_');
const body = JSON.stringify({
tenantId: input.tenantId,
email: input.email.trim(),
password: input.password,
...(input.uuid ? { uuid: input.uuid } : {}),
...(input.code != null && input.code !== '' ? { code: input.code } : {}),
});
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.timeout = 60_000;
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8');
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Accept-Language', language);
xhr.setRequestHeader('Content-Language', language);
if (clientId) {
xhr.setRequestHeader('ClientID', clientId);
}
xhr.onload = () => {
try {
const text = xhr.responseText ?? '';
const json = text ? JSON.parse(text) : null;
if (xhr.status < 200 || xhr.status >= 300) {
try {
unwrapAbpResponse(json);
} catch (e) {
reject(e instanceof Error ? e : new Error(`登录失败 HTTP ${xhr.status}`));
return;
}
reject(new Error(`登录失败 HTTP ${xhr.status}`));
return;
}
const data = unwrapAbpResponse<unknown>(json);
resolve(normalizeLoginOutput(data));
} catch (e) {
reject(
e instanceof Error
? e
: new Error('登录响应解析失败,请查看 Network 中 login 的 Response'),
);
}
};
xhr.onerror = () => {
reject(
new Error(
'登录网络异常:请确认 dev 服务已启动且代理地址正确(/dev-api → saas-test)',
),
);
};
xhr.ontimeout = () => {
reject(
new Error(
'登录请求超时:若 Network 中已有 token 响应,请刷新页面后只点一次登录',
),
);
};
xhr.send(body);
});
}
/** POST /api/app/th-app-auth/login(匿名) */
export async function thAppLogin(
input: ThAppLoginInput,
): Promise<ThAppLoginOutputDto> {
if (loginInFlight) {
return loginInFlight;
}
loginInFlight = thAppLoginXhr(input).finally(() => {
loginInFlight = null;
});
return loginInFlight;
}
/** GET /api/app/th-app-auth/my-locations(Bearer + 租户上下文) */
export async function thAppMyLocations(): Promise<ThAppBoundLocationDto[]> {
const { requestClient } = await import('#/api/request');
const raw = await requestClient.get<unknown>('th-app-auth/my-locations', {
timeout: 30_000,
});
return normalizeLocationList(raw);
}