@liujitcn/kratos-taro-app-core 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/base/v1/file.ts +2 -2
- package/src/components/MfaRecoveryCodesDialog.tsx +13 -1
- package/src/locales/en-US.json +2 -1
- package/src/locales/ja-JP.json +2 -1
- package/src/locales/zh-CN.json +2 -1
- package/src/locales/zh-TW.json +2 -1
- package/src/stores/user.ts +20 -7
- package/src/utils/file.ts +2 -2
- package/src/utils/http.ts +20 -8
- package/src/views/pages/login/login.tsx +15 -12
- package/src/views/pages/webview/webview.tsx +15 -8
package/package.json
CHANGED
package/src/api/base/v1/file.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '../../../rpc/base/v1/file'
|
|
8
8
|
import { uploadFile, uploadFileList } from '../../../utils/file'
|
|
9
9
|
import { getLocaleRequestHeaders, t } from '../../../locales'
|
|
10
|
-
import { getRequestAccessToken, requestBaseURL } from '../../../utils/http'
|
|
10
|
+
import { getRequestAccessToken, requestBaseURL, sourceClient } from '../../../utils/http'
|
|
11
11
|
|
|
12
12
|
const FILE_URL = '/v1/base/file'
|
|
13
13
|
|
|
@@ -51,7 +51,7 @@ export class FileServiceImpl {
|
|
|
51
51
|
header: {
|
|
52
52
|
...getLocaleRequestHeaders(),
|
|
53
53
|
Authorization: token,
|
|
54
|
-
'source-client':
|
|
54
|
+
'source-client': sourceClient,
|
|
55
55
|
},
|
|
56
56
|
})
|
|
57
57
|
if (response.statusCode !== 200) throw new Error(t('core.file.download_failed'))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button, Text, View } from '@tarojs/components'
|
|
2
2
|
import Taro from '@tarojs/taro'
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
3
4
|
import { useI18n } from '../locales'
|
|
4
5
|
import './MfaRecoveryCodesDialog.scss'
|
|
5
6
|
|
|
@@ -20,6 +21,10 @@ export default function MfaRecoveryCodesDialog({
|
|
|
20
21
|
onConfirm,
|
|
21
22
|
}: MfaRecoveryCodesDialogProps) {
|
|
22
23
|
const { t } = useI18n()
|
|
24
|
+
const confirmingRef = useRef(false)
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) confirmingRef.current = false
|
|
27
|
+
}, [open])
|
|
23
28
|
if (!open) return null
|
|
24
29
|
|
|
25
30
|
const codesText = codes.join('\n')
|
|
@@ -31,6 +36,13 @@ export default function MfaRecoveryCodesDialog({
|
|
|
31
36
|
await Taro.showToast({ icon: 'none', title: t('core.login.mfa_recovery_codes_copied') })
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/** 一次性消费确认动作,防止重复点击重复触发绑定流程。 */
|
|
40
|
+
const handleConfirm = () => {
|
|
41
|
+
if (confirmingRef.current) return
|
|
42
|
+
confirmingRef.current = true
|
|
43
|
+
void onConfirm()
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
return (
|
|
35
47
|
<View className='mfa-recovery-dialog'>
|
|
36
48
|
<View className='mfa-recovery-dialog__panel' onClick={(event) => event.stopPropagation()}>
|
|
@@ -51,7 +63,7 @@ export default function MfaRecoveryCodesDialog({
|
|
|
51
63
|
</Text>
|
|
52
64
|
</View>
|
|
53
65
|
<View className='mfa-recovery-dialog__footer'>
|
|
54
|
-
<Button className='mfa-recovery-dialog__confirm' onClick={
|
|
66
|
+
<Button className='mfa-recovery-dialog__confirm' onClick={handleConfirm}>
|
|
55
67
|
{t('core.login.mfa_recovery_codes_confirm')}
|
|
56
68
|
</Button>
|
|
57
69
|
</View>
|
package/src/locales/en-US.json
CHANGED
|
@@ -123,5 +123,6 @@
|
|
|
123
123
|
"common.value.top_level": "Top level",
|
|
124
124
|
"common.resource.default": "{resource}",
|
|
125
125
|
"common.action.delete_resource": "Delete {resource}",
|
|
126
|
-
"common.action.set_field_status": "Set {field}"
|
|
126
|
+
"common.action.set_field_status": "Set {field}",
|
|
127
|
+
"common.action.load_more": "Load more"
|
|
127
128
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -123,5 +123,6 @@
|
|
|
123
123
|
"common.value.top_level": "トップレベル",
|
|
124
124
|
"common.resource.default": "{resource}",
|
|
125
125
|
"common.action.delete_resource": "{resource}を削除",
|
|
126
|
-
"common.action.set_field_status": "{field}を設定"
|
|
126
|
+
"common.action.set_field_status": "{field}を設定",
|
|
127
|
+
"common.action.load_more": "さらに読み込む"
|
|
127
128
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -123,5 +123,6 @@
|
|
|
123
123
|
"common.value.top_level": "顶级节点",
|
|
124
124
|
"common.resource.default": "{resource}",
|
|
125
125
|
"common.action.delete_resource": "删除{resource}",
|
|
126
|
-
"common.action.set_field_status": "设置{field}"
|
|
126
|
+
"common.action.set_field_status": "设置{field}",
|
|
127
|
+
"common.action.load_more": "加载更多"
|
|
127
128
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -123,5 +123,6 @@
|
|
|
123
123
|
"common.value.top_level": "頂級節點",
|
|
124
124
|
"common.resource.default": "{resource}",
|
|
125
125
|
"common.action.delete_resource": "刪除{resource}",
|
|
126
|
-
"common.action.set_field_status": "設定{field}"
|
|
126
|
+
"common.action.set_field_status": "設定{field}",
|
|
127
|
+
"common.action.load_more": "載入更多"
|
|
127
128
|
}
|
package/src/stores/user.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { create } from 'zustand'
|
|
|
3
3
|
import { defLoginService } from '../api/base/v1/login'
|
|
4
4
|
import { defOauthService } from '../api/base/v1/oauth'
|
|
5
5
|
import { defAuthService } from '../api/system/app/v1/auth'
|
|
6
|
-
import type
|
|
6
|
+
import { LoginStatus, type LoginRequest, type LoginResponse } from '../rpc/base/v1/login'
|
|
7
7
|
import type { VerifyMfaRequest } from '../rpc/base/v1/mfa'
|
|
8
8
|
import { defMfaService } from '../api/base/v1/mfa'
|
|
9
9
|
import type {
|
|
@@ -80,6 +80,15 @@ function persistUserInfo(userInfo?: UserProfileForm): void {
|
|
|
80
80
|
else Taro.removeStorageSync(USER_STORAGE_KEY)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** 仅当登录阶段已签发非空令牌时,才允许写入本地登录态。 */
|
|
84
|
+
function shouldApplyLoginToken(data: LoginResponse | CreateOauthSessionResponse): boolean {
|
|
85
|
+
return (
|
|
86
|
+
(data.status === LoginStatus.LOGIN_STATUS_AUTHENTICATED ||
|
|
87
|
+
data.status === LoginStatus.LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED) &&
|
|
88
|
+
Boolean(data.access_token && data.refresh_token)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
/** 用户 Zustand Store。 */
|
|
84
93
|
export const useUserStore = create<UserStoreState>((set, get) => ({
|
|
85
94
|
userInfo: undefined,
|
|
@@ -105,22 +114,22 @@ export const useUserStore = create<UserStoreState>((set, get) => ({
|
|
|
105
114
|
},
|
|
106
115
|
async login(request) {
|
|
107
116
|
const response = await defLoginService.Login(request)
|
|
108
|
-
if (response
|
|
117
|
+
if (shouldApplyLoginToken(response)) await get().applyLoginToken(response)
|
|
109
118
|
return response
|
|
110
119
|
},
|
|
111
120
|
async verifyMfa(request) {
|
|
112
121
|
const response = await defMfaService.VerifyMfa(request)
|
|
113
|
-
await get().applyLoginToken(response)
|
|
122
|
+
if (shouldApplyLoginToken(response)) await get().applyLoginToken(response)
|
|
114
123
|
return response
|
|
115
124
|
},
|
|
116
125
|
async createOauthSession(request) {
|
|
117
126
|
const response = await defOauthService.CreateOauthSession(request)
|
|
118
|
-
if (!response.binding_required && (response
|
|
127
|
+
if (!response.binding_required && shouldApplyLoginToken(response)) await get().applyLoginToken(response)
|
|
119
128
|
return response
|
|
120
129
|
},
|
|
121
130
|
async bindOauthSession(request) {
|
|
122
131
|
const response = await defOauthService.BindOauthSession(request)
|
|
123
|
-
if (response
|
|
132
|
+
if (shouldApplyLoginToken(response)) await get().applyLoginToken(response)
|
|
124
133
|
return response
|
|
125
134
|
},
|
|
126
135
|
async getUserProfile() {
|
|
@@ -131,8 +140,12 @@ export const useUserStore = create<UserStoreState>((set, get) => ({
|
|
|
131
140
|
return profile
|
|
132
141
|
},
|
|
133
142
|
async logout() {
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
try {
|
|
144
|
+
await defLoginService.Logout({})
|
|
145
|
+
} finally {
|
|
146
|
+
// 登出接口失败时仍清理本地登录态,避免残留有效 token 造成“假登录”。
|
|
147
|
+
await get().clearUserData()
|
|
148
|
+
}
|
|
136
149
|
},
|
|
137
150
|
async refreshToken() {
|
|
138
151
|
const response = await defLoginService.RefreshToken({ refresh_token: getRefreshToken() })
|
package/src/utils/file.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Taro from '@tarojs/taro'
|
|
|
2
2
|
import { BaseFileAccessMode, type FileInfo, type MultiUploadFileResponse } from '../rpc/base/v1/file'
|
|
3
3
|
import { getLocaleRequestHeaders, t } from '../locales'
|
|
4
4
|
import { formatSrc } from './index'
|
|
5
|
-
import { getRequestAccessToken, requestBaseURL } from './http'
|
|
5
|
+
import { getRequestAccessToken, requestBaseURL, sourceClient } from './http'
|
|
6
6
|
|
|
7
7
|
/** 上传单个文件。 */
|
|
8
8
|
export async function uploadFile(
|
|
@@ -18,7 +18,7 @@ export async function uploadFile(
|
|
|
18
18
|
formData: { fileType, accessMode: String(accessMode) },
|
|
19
19
|
header: {
|
|
20
20
|
...getLocaleRequestHeaders(),
|
|
21
|
-
'source-client':
|
|
21
|
+
'source-client': sourceClient,
|
|
22
22
|
...(token ? { Authorization: token } : {}),
|
|
23
23
|
},
|
|
24
24
|
})
|
package/src/utils/http.ts
CHANGED
|
@@ -18,7 +18,14 @@ const normalizedApiBasePath = apiBasePath.startsWith('/') ? apiBasePath : `/${ap
|
|
|
18
18
|
const requestOrigin = process.env.TARO_ENV === 'h5' ? '' : apiTargetUrl.replace(/\/$/, '')
|
|
19
19
|
/** 请求基础地址。 */
|
|
20
20
|
export const requestBaseURL = `${requestOrigin}${normalizedApiBasePath}`
|
|
21
|
-
|
|
21
|
+
/** 站点根地址(不含 API 基础路径),供事件流等非 API 端点复用。 */
|
|
22
|
+
export const siteBaseURL =
|
|
23
|
+
process.env.TARO_ENV === 'h5'
|
|
24
|
+
? typeof window !== 'undefined'
|
|
25
|
+
? window.location.origin
|
|
26
|
+
: ''
|
|
27
|
+
: apiTargetUrl.replace(/\/$/, '')
|
|
28
|
+
export const sourceClient = process.env.TARO_ENV === 'weapp' ? 'taro-weapp' : 'taro-h5'
|
|
22
29
|
|
|
23
30
|
const SESSION_URL = '/v1/base/session'
|
|
24
31
|
const REFRESH_TOKEN_URL = '/v1/base/token'
|
|
@@ -176,7 +183,13 @@ async function getAccessTokenByMode(authMode: AuthMode): Promise<string> {
|
|
|
176
183
|
}
|
|
177
184
|
return ''
|
|
178
185
|
}
|
|
179
|
-
if (shouldRefreshToken())
|
|
186
|
+
if (shouldRefreshToken()) {
|
|
187
|
+
try {
|
|
188
|
+
await handleTokenRefresh()
|
|
189
|
+
} catch {
|
|
190
|
+
// 刷新失败已撤销本地认证,交由下方 hasValidToken 分支按当前调用方 authMode 处理。
|
|
191
|
+
}
|
|
192
|
+
}
|
|
180
193
|
if (hasValidToken()) return getToken()
|
|
181
194
|
if (authMode === 'required') {
|
|
182
195
|
await promptRelogin()
|
|
@@ -186,12 +199,12 @@ async function getAccessTokenByMode(authMode: AuthMode): Promise<string> {
|
|
|
186
199
|
return ''
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
function handleTokenRefresh(
|
|
202
|
+
function handleTokenRefresh(): Promise<void> {
|
|
190
203
|
if (refreshTokenPromise) return refreshTokenPromise
|
|
191
204
|
refreshTokenPromise = refreshAccessToken()
|
|
192
|
-
.catch(
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
.catch((error) => {
|
|
206
|
+
// 刷新失败后立即撤销本地认证,避免弹窗等待期间后台请求反复提交旧刷新令牌。
|
|
207
|
+
silentClearAuthData()
|
|
195
208
|
throw error
|
|
196
209
|
})
|
|
197
210
|
.finally(() => {
|
|
@@ -243,8 +256,7 @@ async function promptRelogin(): Promise<void> {
|
|
|
243
256
|
})
|
|
244
257
|
if (!modal.confirm) return
|
|
245
258
|
await new Promise((resolve) => setTimeout(resolve, 80))
|
|
246
|
-
|
|
247
|
-
Taro.removeStorageSync('user')
|
|
259
|
+
silentClearAuthData()
|
|
248
260
|
saveCurrentRoute()
|
|
249
261
|
await Taro.reLaunch({ url: '/pages/login/login' })
|
|
250
262
|
} finally {
|
|
@@ -37,16 +37,8 @@ const emptyLoginForm = (): LoginRequest => ({
|
|
|
37
37
|
|
|
38
38
|
function isWechatUnboundError(error: unknown): boolean {
|
|
39
39
|
if (!error || typeof error !== 'object') return false
|
|
40
|
-
const response = error as {
|
|
41
|
-
|
|
42
|
-
reason?: string | number
|
|
43
|
-
message?: string
|
|
44
|
-
binding_required?: boolean
|
|
45
|
-
error?: { reason?: string | number; message?: string }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const reason = response.data?.reason ?? response.data?.error?.reason
|
|
49
|
-
return response.data?.binding_required === true || String(reason || '') === 'UNAUTHENTICATED'
|
|
40
|
+
const response = error as { data?: { binding_required?: boolean } }
|
|
41
|
+
return response.data?.binding_required === true
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
/** 提取小程序请求失败时后端返回的可展示消息。 */
|
|
@@ -120,6 +112,7 @@ export default function LoginPage() {
|
|
|
120
112
|
const [mfaSetupWebAuthnOptionsJson, setMfaSetupWebAuthnOptionsJson] = useState('')
|
|
121
113
|
const [mfaRecoveryCodesVisible, setMfaRecoveryCodesVisible] = useState(false)
|
|
122
114
|
const [mfaRecoveryCodes, setMfaRecoveryCodes] = useState<string[]>([])
|
|
115
|
+
const shakeTimerRef = useRef<ReturnType<typeof setTimeout>>()
|
|
123
116
|
|
|
124
117
|
const currentLanguageName =
|
|
125
118
|
languageOptions.find((item) => item.language_code === locale)?.native_name || locale
|
|
@@ -178,7 +171,13 @@ export default function LoginPage() {
|
|
|
178
171
|
|
|
179
172
|
useEffect(() => {
|
|
180
173
|
void Taro.setNavigationBarTitle({ title: t('common.action.login') })
|
|
181
|
-
}, [locale
|
|
174
|
+
}, [locale])
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
return () => {
|
|
178
|
+
if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
|
|
179
|
+
}
|
|
180
|
+
}, [])
|
|
182
181
|
|
|
183
182
|
const updateForm = (key: keyof LoginRequest, value: string) => {
|
|
184
183
|
setForm((current) => ({ ...current, [key]: value }))
|
|
@@ -279,7 +278,11 @@ export default function LoginPage() {
|
|
|
279
278
|
}
|
|
280
279
|
if (agreed) return true
|
|
281
280
|
setShake(true)
|
|
282
|
-
|
|
281
|
+
if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
|
|
282
|
+
shakeTimerRef.current = setTimeout(() => {
|
|
283
|
+
shakeTimerRef.current = undefined
|
|
284
|
+
setShake(false)
|
|
285
|
+
}, 500)
|
|
283
286
|
const result = await Taro.showModal({
|
|
284
287
|
title: t('common.title.notice'),
|
|
285
288
|
content: t('core.login.protocol_prompt'),
|
|
@@ -4,6 +4,17 @@ import { useEffect, useState } from 'react'
|
|
|
4
4
|
import './webview.scss'
|
|
5
5
|
import { useI18n } from '../../../locales'
|
|
6
6
|
|
|
7
|
+
/** 只放行 http(s) 外链,其它协议按非法地址处理。 */
|
|
8
|
+
const isAllowedUrl = (value: string) => /^https?:\/\//i.test(value)
|
|
9
|
+
|
|
10
|
+
const safeDecodeURIComponent = (value: string) => {
|
|
11
|
+
try {
|
|
12
|
+
return decodeURIComponent(value)
|
|
13
|
+
} catch {
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
/** 统一外链承载页。 */
|
|
8
19
|
export default function WebViewPage() {
|
|
9
20
|
const { t } = useI18n()
|
|
@@ -13,9 +24,9 @@ export default function WebViewPage() {
|
|
|
13
24
|
const isH5 = process.env.TARO_ENV === 'h5'
|
|
14
25
|
|
|
15
26
|
useLoad((query) => {
|
|
16
|
-
const nextUrl =
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
const nextUrl = safeDecodeURIComponent(query?.url || '')
|
|
28
|
+
setUrl(isAllowedUrl(nextUrl) ? nextUrl : '')
|
|
29
|
+
const title = safeDecodeURIComponent(query?.title || '')
|
|
19
30
|
if (title) void Taro.setNavigationBarTitle({ title })
|
|
20
31
|
})
|
|
21
32
|
|
|
@@ -35,11 +46,7 @@ export default function WebViewPage() {
|
|
|
35
46
|
return (
|
|
36
47
|
<View className='webview-container'>
|
|
37
48
|
{url ? (
|
|
38
|
-
<WebView
|
|
39
|
-
src={url}
|
|
40
|
-
onLoad={() => setIframeLoaded(true)}
|
|
41
|
-
onMessage={(event) => console.log('收到H5消息:', event.detail)}
|
|
42
|
-
/>
|
|
49
|
+
<WebView src={url} onLoad={() => setIframeLoaded(true)} />
|
|
43
50
|
) : null}
|
|
44
51
|
{showFallback ? (
|
|
45
52
|
<View className='webview-empty'>
|