@liujitcn/kratos-uni-app-core 0.0.28 → 0.0.31
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/README.md +28 -5
- package/dist/module.mjs +164 -4
- package/package.json +3 -1
- package/src/api/base/mfa.ts +127 -0
- package/src/components/KratosSettingsPage.vue +280 -0
- package/src/components/KratosTabBar.vue +50 -1
- package/src/components/MfaManageDialog.vue +469 -0
- package/src/components/MfaRecoveryCodesDialog.vue +192 -0
- package/src/components/MfaSetupPanel.vue +160 -0
- package/src/components/PasswordVerifyDialog.vue +277 -0
- package/src/index.ts +2 -0
- package/src/locales/README.md +2 -1
- package/src/locales/en-US.json +41 -1
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +41 -1
- package/src/locales/zh-CN.json +41 -1
- package/src/locales/zh-TW.json +41 -1
- package/src/navigation.ts +47 -1
- package/src/rpc/base/v1/login.ts +33 -2
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/stores/modules/user.ts +26 -7
- package/src/utils/clipboard.ts +33 -0
- package/src/utils/http.ts +8 -3
- package/src/utils/webauthn.ts +101 -0
- package/src/views/pages/login/login.vue +293 -12
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { useUserStore } from '../stores'
|
|
4
|
+
import { navigateToLogin } from '../utils/navigation'
|
|
5
|
+
import { getLanguageOptions, useI18n, type SupportedLocale } from '../locales'
|
|
6
|
+
import { defMfaService } from '../api/base/mfa'
|
|
7
|
+
import MfaManageDialog from './MfaManageDialog.vue'
|
|
8
|
+
|
|
9
|
+
defineOptions({ name: 'KratosSettingsPage' })
|
|
10
|
+
|
|
11
|
+
defineSlots<{
|
|
12
|
+
/** 在所有公共设置卡片之前渲染的业务扩展内容。 */
|
|
13
|
+
extensions?: () => unknown
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const userStore = useUserStore()
|
|
17
|
+
const { locale, setLocale, t } = useI18n()
|
|
18
|
+
const logoutLoading = ref(false)
|
|
19
|
+
const mfaDialogVisible = ref(false)
|
|
20
|
+
const mfaDialogMode = ref<'setup' | 'disable'>('setup')
|
|
21
|
+
const mfaEnabled = ref(false)
|
|
22
|
+
const mfaMethod = ref('totp')
|
|
23
|
+
const mfaStatusLoading = ref(false)
|
|
24
|
+
const localeOptions = computed(() => getLanguageOptions())
|
|
25
|
+
const localeLabels = computed(() => localeOptions.value.map((item) => item.native_name))
|
|
26
|
+
const localeIndex = computed(() =>
|
|
27
|
+
Math.max(
|
|
28
|
+
0,
|
|
29
|
+
localeOptions.value.findIndex((item) => item.language_code === locale.value),
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
watch(locale, () => void uni.setNavigationBarTitle({ title: t('core.settings.title') }), {
|
|
34
|
+
immediate: true,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** 处理语言选择并刷新动态本地化数据。 */
|
|
38
|
+
const onLocaleChange = (event: { detail: { value: string | number } }) => {
|
|
39
|
+
const nextLocale = localeOptions.value[Number(event.detail.value)]?.language_code as
|
|
40
|
+
| SupportedLocale
|
|
41
|
+
| undefined
|
|
42
|
+
if (nextLocale) void setLocale(nextLocale)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 加载当前用户的多因素认证状态。 */
|
|
46
|
+
const loadMfaStatus = async (force = false) => {
|
|
47
|
+
if (!userStore.isAuthenticated() || (mfaStatusLoading.value && !force)) return
|
|
48
|
+
mfaStatusLoading.value = true
|
|
49
|
+
try {
|
|
50
|
+
const result = await defMfaService.GetMfaStatus({})
|
|
51
|
+
mfaEnabled.value = result.enabled
|
|
52
|
+
mfaMethod.value = result.method || 'totp'
|
|
53
|
+
} finally {
|
|
54
|
+
mfaStatusLoading.value = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 当前多因素认证状态展示文案。 */
|
|
59
|
+
const mfaStatusText = computed(() => {
|
|
60
|
+
if (!mfaEnabled.value) return t('core.settings.mfa_disabled')
|
|
61
|
+
const method =
|
|
62
|
+
mfaMethod.value === 'webauthn'
|
|
63
|
+
? t('core.settings.mfa_method_webauthn')
|
|
64
|
+
: t('core.settings.mfa_method_totp')
|
|
65
|
+
return t('core.settings.mfa_enabled_method', { method })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/** 根据当前状态打开多因素认证操作组件。 */
|
|
69
|
+
const onMfaTap = async () => {
|
|
70
|
+
if (!userStore.ensureAuthenticated()) {
|
|
71
|
+
navigateToLogin()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const currentMfaEnabled = mfaEnabled.value
|
|
75
|
+
try {
|
|
76
|
+
await loadMfaStatus(true)
|
|
77
|
+
} catch {
|
|
78
|
+
// 状态刷新失败时保留页面已有状态,避免已启用用户无法进入禁用流程。
|
|
79
|
+
mfaEnabled.value = currentMfaEnabled
|
|
80
|
+
}
|
|
81
|
+
if (mfaEnabled.value) {
|
|
82
|
+
mfaDialogMode.value = 'disable'
|
|
83
|
+
mfaDialogVisible.value = true
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
mfaDialogMode.value = 'setup'
|
|
87
|
+
mfaDialogVisible.value = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 绑定成功后清理旧会话并重新登录。 */
|
|
91
|
+
const onMfaSuccess = async (method: string) => {
|
|
92
|
+
mfaEnabled.value = true
|
|
93
|
+
mfaMethod.value = method || 'totp'
|
|
94
|
+
await uni.showToast({ icon: 'none', title: t('core.settings.mfa_bind_success') })
|
|
95
|
+
// 绑定 MFA 后后端会撤销当前会话,不能再调用登出接口或继续使用旧 token。
|
|
96
|
+
await userStore.clearUserData()
|
|
97
|
+
uni.reLaunch({ url: '/pages/login/login' })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 禁用成功后清理旧会话并重新登录。 */
|
|
101
|
+
const onMfaDisabled = async () => {
|
|
102
|
+
mfaEnabled.value = false
|
|
103
|
+
await uni.showToast({ icon: 'none', title: t('core.settings.mfa_disable_success') })
|
|
104
|
+
await userStore.clearUserData()
|
|
105
|
+
uni.reLaunch({ url: '/pages/login/login' })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 公共设置模块挂载时加载 MFA 状态,兼容页面包装和业务插槽场景。
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
if (!userStore.ensureAuthenticated()) {
|
|
111
|
+
// #ifndef MP-WEIXIN
|
|
112
|
+
navigateToLogin()
|
|
113
|
+
// #endif
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
void loadMfaStatus()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/** 确认后退出登录并返回上一页。 */
|
|
120
|
+
const onLogout = () => {
|
|
121
|
+
if (logoutLoading.value) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
// 模态弹窗
|
|
125
|
+
uni.showModal({
|
|
126
|
+
content: t('core.settings.logout_confirm'),
|
|
127
|
+
confirmColor: '#27BA9B',
|
|
128
|
+
success: async (res) => {
|
|
129
|
+
if (!res.confirm) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logoutLoading.value = true
|
|
134
|
+
try {
|
|
135
|
+
// 先完成退出和本地登录态清理,再返回个人中心,避免 onShow 读取到旧登录态。
|
|
136
|
+
await userStore.logout()
|
|
137
|
+
uni.navigateBack()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await uni.showToast({
|
|
140
|
+
icon: 'none',
|
|
141
|
+
title: t('core.settings.logout_failed'),
|
|
142
|
+
})
|
|
143
|
+
} finally {
|
|
144
|
+
logoutLoading.value = false
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<view class="viewport">
|
|
153
|
+
<slot name="extensions" />
|
|
154
|
+
<view class="list">
|
|
155
|
+
<view v-if="userStore.isAuthenticated()" class="item arrow mfa-item" @tap="onMfaTap">
|
|
156
|
+
<text>{{ t('core.settings.mfa_title') }}</text>
|
|
157
|
+
<text class="mfa-value">{{ mfaStatusText }}</text>
|
|
158
|
+
</view>
|
|
159
|
+
<picker :range="localeLabels" :value="localeIndex" @change="onLocaleChange">
|
|
160
|
+
<view class="item arrow locale-item">
|
|
161
|
+
<text>{{ t('common.field.language') }}</text>
|
|
162
|
+
<text class="locale-value">{{ localeLabels[localeIndex] }}</text>
|
|
163
|
+
</view>
|
|
164
|
+
</picker>
|
|
165
|
+
</view>
|
|
166
|
+
<!-- #ifdef MP-WEIXIN -->
|
|
167
|
+
<!-- 列表2 -->
|
|
168
|
+
<view class="list">
|
|
169
|
+
<button hover-class="none" class="item arrow" open-type="openSetting">
|
|
170
|
+
{{ t('core.settings.authorization') }}
|
|
171
|
+
</button>
|
|
172
|
+
<button hover-class="none" class="item arrow" open-type="feedback">
|
|
173
|
+
{{ t('core.settings.feedback') }}
|
|
174
|
+
</button>
|
|
175
|
+
<button hover-class="none" class="item arrow" open-type="contact">
|
|
176
|
+
{{ t('core.settings.contact') }}
|
|
177
|
+
</button>
|
|
178
|
+
</view>
|
|
179
|
+
<!-- #endif -->
|
|
180
|
+
<!-- 操作按钮 -->
|
|
181
|
+
<view class="action" v-if="userStore.isAuthenticated()">
|
|
182
|
+
<view @tap="onLogout" class="button">{{ t('common.action.logout') }}</view>
|
|
183
|
+
</view>
|
|
184
|
+
</view>
|
|
185
|
+
<MfaManageDialog
|
|
186
|
+
v-model="mfaDialogVisible"
|
|
187
|
+
:mode="mfaDialogMode"
|
|
188
|
+
:method="mfaMethod"
|
|
189
|
+
@success="onMfaSuccess"
|
|
190
|
+
@disabled="onMfaDisabled"
|
|
191
|
+
/>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<style scoped lang="scss">
|
|
195
|
+
.viewport {
|
|
196
|
+
min-height: 100vh;
|
|
197
|
+
padding: 20rpx;
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
background-color: #f4f4f4;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* 列表 */
|
|
203
|
+
.list {
|
|
204
|
+
padding: 0 20rpx;
|
|
205
|
+
background-color: #fff;
|
|
206
|
+
margin-bottom: 20rpx;
|
|
207
|
+
border-radius: 10rpx;
|
|
208
|
+
.item {
|
|
209
|
+
line-height: 90rpx;
|
|
210
|
+
padding-left: 10rpx;
|
|
211
|
+
font-size: 30rpx;
|
|
212
|
+
color: #333;
|
|
213
|
+
border-top: 1rpx solid #ddd;
|
|
214
|
+
position: relative;
|
|
215
|
+
text-align: left;
|
|
216
|
+
border-radius: 0;
|
|
217
|
+
background-color: #fff;
|
|
218
|
+
&::after {
|
|
219
|
+
width: auto;
|
|
220
|
+
height: auto;
|
|
221
|
+
left: auto;
|
|
222
|
+
border: none;
|
|
223
|
+
}
|
|
224
|
+
&::after {
|
|
225
|
+
right: 5rpx;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
> .item:first-child {
|
|
229
|
+
border: none;
|
|
230
|
+
}
|
|
231
|
+
.arrow::after {
|
|
232
|
+
content: '›';
|
|
233
|
+
position: absolute;
|
|
234
|
+
top: 50%;
|
|
235
|
+
color: #ccc;
|
|
236
|
+
font-size: 36rpx;
|
|
237
|
+
transform: translateY(-50%);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.locale-item {
|
|
242
|
+
display: flex;
|
|
243
|
+
justify-content: space-between;
|
|
244
|
+
padding-right: 42rpx;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.locale-value {
|
|
248
|
+
color: #888;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.mfa-item {
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
justify-content: space-between;
|
|
255
|
+
padding-right: 42rpx;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.mfa-value {
|
|
259
|
+
max-width: 55%;
|
|
260
|
+
overflow: hidden;
|
|
261
|
+
color: #888;
|
|
262
|
+
font-size: 26rpx;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* 操作按钮 */
|
|
268
|
+
.action {
|
|
269
|
+
text-align: center;
|
|
270
|
+
line-height: 90rpx;
|
|
271
|
+
margin-top: 40rpx;
|
|
272
|
+
font-size: 32rpx;
|
|
273
|
+
color: #333;
|
|
274
|
+
.button {
|
|
275
|
+
background-color: #fff;
|
|
276
|
+
margin-bottom: 20rpx;
|
|
277
|
+
border-radius: 10rpx;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { resolveModuleIcon, resolveStaticView } from '../module'
|
|
4
|
-
import { APP_MENU_ROOT_ID, useAppNavigation } from '../navigation'
|
|
4
|
+
import { APP_MENU_ROOT_ID, useAppMenuBadge, useAppNavigation } from '../navigation'
|
|
5
5
|
import { resolveRootMenuId } from '../navigation-tree.mjs'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{ route: string }>()
|
|
@@ -13,6 +13,7 @@ const activeMenu = computed(() => {
|
|
|
13
13
|
return tabBar.value.find((menu) => menu.id === tabMenuId)
|
|
14
14
|
})
|
|
15
15
|
const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
16
|
+
const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
|
|
16
17
|
</script>
|
|
17
18
|
|
|
18
19
|
<template>
|
|
@@ -23,6 +24,9 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
23
24
|
class="kratos-tab-bar__item"
|
|
24
25
|
@tap="navigate(item.path)"
|
|
25
26
|
>
|
|
27
|
+
<text v-if="item.viewKey === 'MESSAGE_INBOX' && messageBadge" class="kratos-tab-bar__badge">{{
|
|
28
|
+
messageBadge
|
|
29
|
+
}}</text>
|
|
26
30
|
<image
|
|
27
31
|
v-if="resolveModuleIcon(item.icon)"
|
|
28
32
|
class="kratos-tab-bar__icon"
|
|
@@ -33,6 +37,12 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
33
37
|
"
|
|
34
38
|
mode="aspectFit"
|
|
35
39
|
/>
|
|
40
|
+
<view
|
|
41
|
+
v-else-if="item.viewKey === 'MESSAGE_INBOX'"
|
|
42
|
+
class="kratos-tab-bar__message-icon"
|
|
43
|
+
:class="{ 'kratos-tab-bar__message-icon--active': activeMenu?.id === item.id }"
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
/>
|
|
36
46
|
<text :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }">
|
|
37
47
|
{{ item.title }}
|
|
38
48
|
</text>
|
|
@@ -55,6 +65,7 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
55
65
|
background: #fff;
|
|
56
66
|
}
|
|
57
67
|
.kratos-tab-bar__item {
|
|
68
|
+
position: relative;
|
|
58
69
|
display: flex;
|
|
59
70
|
flex: 1;
|
|
60
71
|
align-items: center;
|
|
@@ -70,7 +81,45 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
70
81
|
height: 28px;
|
|
71
82
|
margin-bottom: 4px;
|
|
72
83
|
}
|
|
84
|
+
.kratos-tab-bar__message-icon {
|
|
85
|
+
position: relative;
|
|
86
|
+
width: 22px;
|
|
87
|
+
height: 16px;
|
|
88
|
+
margin-bottom: 8px;
|
|
89
|
+
border: 2px solid #9aa4b2;
|
|
90
|
+
border-radius: 3px;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
}
|
|
93
|
+
.kratos-tab-bar__message-icon::after {
|
|
94
|
+
position: absolute;
|
|
95
|
+
top: 1px;
|
|
96
|
+
left: 4px;
|
|
97
|
+
width: 9px;
|
|
98
|
+
height: 9px;
|
|
99
|
+
border-right: 2px solid #9aa4b2;
|
|
100
|
+
border-bottom: 2px solid #9aa4b2;
|
|
101
|
+
content: '';
|
|
102
|
+
transform: rotate(45deg);
|
|
103
|
+
}
|
|
104
|
+
.kratos-tab-bar__message-icon--active,
|
|
105
|
+
.kratos-tab-bar__message-icon--active::after {
|
|
106
|
+
border-color: #27ba9b;
|
|
107
|
+
}
|
|
73
108
|
.kratos-tab-bar__text--active {
|
|
74
109
|
color: #27ba9b;
|
|
75
110
|
}
|
|
111
|
+
.kratos-tab-bar__badge {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 2px;
|
|
114
|
+
right: calc(50% - 22px);
|
|
115
|
+
min-width: 14px;
|
|
116
|
+
height: 14px;
|
|
117
|
+
padding: 0 3px;
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
background: #e5484d;
|
|
120
|
+
color: #fff;
|
|
121
|
+
font-size: 9px;
|
|
122
|
+
line-height: 14px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
76
125
|
</style>
|