@liujitcn/kratos-admin-core 0.0.29 → 0.0.32

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.
Files changed (147) hide show
  1. package/README.md +15 -8
  2. package/build/auto-imports.json +73 -0
  3. package/build/plugins.ts +2 -58
  4. package/dist/package/declarations/src/api/base/{config.d.ts → v1/config.d.ts} +1 -1
  5. package/dist/package/declarations/src/api/base/{file.d.ts → v1/file.d.ts} +3 -1
  6. package/dist/package/declarations/src/api/base/{language.d.ts → v1/language.d.ts} +1 -1
  7. package/dist/package/declarations/src/api/base/{login.d.ts → v1/login.d.ts} +2 -2
  8. package/dist/package/declarations/src/api/base/v1/mfa.d.ts +26 -0
  9. package/dist/package/declarations/src/api/base/{oauth.d.ts → v1/oauth.d.ts} +1 -1
  10. package/dist/package/declarations/src/api/system/{auth.d.ts → admin/v1/auth.d.ts} +2 -2
  11. package/dist/package/declarations/src/api/system/{base_dict.d.ts → admin/v1/base_dict.d.ts} +1 -1
  12. package/dist/package/declarations/src/api/system/{base_tenant.d.ts → admin/v1/base_tenant.d.ts} +2 -2
  13. package/dist/package/declarations/src/components/Dialog/FormDialog.vue.d.ts +3 -3
  14. package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +65 -0
  15. package/dist/package/declarations/src/components/Dialog/ProDialog.vue.d.ts +1 -1
  16. package/dist/package/declarations/src/components/Grid/components/GridItem.vue.d.ts +1 -1
  17. package/dist/package/declarations/src/components/Mfa/MfaRecoveryCodesDialog.vue.d.ts +19 -0
  18. package/dist/package/declarations/src/components/Mfa/MfaSetupPanel.vue.d.ts +8 -0
  19. package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +4 -4
  20. package/dist/package/declarations/src/components/ProForm/index.vue.d.ts +1 -1
  21. package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +4 -1
  22. package/dist/package/declarations/src/components/ProTable/index.vue.d.ts +114 -0
  23. package/dist/package/declarations/src/components/RichTextPreview/index.vue.d.ts +8 -0
  24. package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +84 -76
  25. package/dist/package/declarations/src/components/Upload/File.vue.d.ts +1 -1
  26. package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +1 -1
  27. package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +2 -2
  28. package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +2 -2
  29. package/dist/package/declarations/src/components/WangEditor/index.vue.d.ts +2 -0
  30. package/dist/package/declarations/src/documentTitle.d.ts +2 -0
  31. package/dist/package/declarations/src/format.d.ts +1 -1
  32. package/dist/package/declarations/src/index.d.ts +2 -1
  33. package/dist/package/declarations/src/layouts/components/LockScreen.vue.d.ts +3 -0
  34. package/dist/package/declarations/src/locales/generated.d.ts +208 -0
  35. package/dist/package/declarations/src/locales/index.d.ts +2 -0
  36. package/dist/package/declarations/src/modules/index.d.ts +1 -3
  37. package/dist/package/declarations/src/request.d.ts +1 -1
  38. package/dist/package/declarations/src/routers/index.d.ts +1 -1
  39. package/dist/package/declarations/src/rpc/base/v1/login.d.ts +37 -5
  40. package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +152 -0
  41. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +25 -0
  42. package/dist/package/declarations/src/rpc/base/v1/oauth_client.d.ts +23 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +13 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +0 -101
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +10 -6
  46. package/dist/package/declarations/src/rpc/system/common/v1/common.d.ts +26 -0
  47. package/dist/package/declarations/src/security.d.ts +2 -0
  48. package/dist/package/declarations/src/stores/interface/index.d.ts +19 -1
  49. package/dist/package/declarations/src/stores/modules/lockScreen.d.ts +20 -0
  50. package/dist/package/declarations/src/stores/modules/user.d.ts +7 -6
  51. package/dist/package/declarations/src/stores/runtime.d.ts +1 -0
  52. package/dist/package/declarations/src/utils/clipboard.d.ts +2 -0
  53. package/dist/package/declarations/src/utils/passwordStrength.d.ts +1 -12
  54. package/dist/package/declarations/src/utils/request.d.ts +13 -2
  55. package/dist/package/declarations/src/utils/utils.d.ts +4 -0
  56. package/dist/package/declarations/src/utils/webauthn.d.ts +4 -0
  57. package/dist/package/declarations/vite.config.d.ts +2 -0
  58. package/dist/package/src/api/base/{config.ts → v1/config.ts} +2 -2
  59. package/dist/package/src/api/base/{file.ts → v1/file.ts} +20 -24
  60. package/dist/package/src/api/base/{language.ts → v1/language.ts} +2 -2
  61. package/dist/package/src/api/base/{login.ts → v1/login.ts} +3 -3
  62. package/dist/package/src/api/base/v1/mfa.ts +114 -0
  63. package/dist/package/src/api/base/{oauth.ts → v1/oauth.ts} +2 -2
  64. package/dist/package/src/api/system/{auth.ts → admin/v1/auth.ts} +3 -3
  65. package/dist/package/src/api/system/{base_dict.ts → admin/v1/base_dict.ts} +2 -2
  66. package/dist/package/src/api/system/{base_tenant.ts → admin/v1/base_tenant.ts} +3 -3
  67. package/dist/package/src/bootstrap.ts +6 -5
  68. package/dist/package/src/components/CronExpression/index.vue +4 -3
  69. package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +176 -0
  70. package/dist/package/src/components/Dialog/ProDialog.vue +1 -0
  71. package/dist/package/src/components/ImportExcel/index.vue +4 -2
  72. package/dist/package/src/components/Loading/fullScreen.ts +2 -1
  73. package/dist/package/src/components/MarkdownPreview/index.vue +22 -2
  74. package/dist/package/src/components/Mfa/MfaRecoveryCodesDialog.vue +91 -0
  75. package/dist/package/src/components/Mfa/MfaSetupPanel.vue +81 -0
  76. package/dist/package/src/components/PasswordStrength/index.vue +1 -1
  77. package/dist/package/src/components/ProForm/components/ProFormItem.vue +16 -1
  78. package/dist/package/src/components/ProForm/interface/index.ts +4 -0
  79. package/dist/package/src/components/ProTable/components/ColSetting.vue +1 -1
  80. package/dist/package/src/components/ProTable/components/TableColumn.vue +14 -6
  81. package/dist/package/src/components/ProTable/index.vue +9 -5
  82. package/dist/package/src/components/RichTextPreview/index.vue +56 -0
  83. package/dist/package/src/components/SelectIcon/index.vue +3 -2
  84. package/dist/package/src/components/Upload/File.vue +2 -2
  85. package/dist/package/src/components/Upload/Files.vue +2 -2
  86. package/dist/package/src/components/Upload/Img.vue +7 -5
  87. package/dist/package/src/components/Upload/Imgs.vue +6 -5
  88. package/dist/package/src/components/WangEditor/index.vue +10 -7
  89. package/dist/package/src/config/index.ts +1 -1
  90. package/dist/package/src/documentTitle.ts +8 -0
  91. package/dist/package/src/format.ts +1 -1
  92. package/dist/package/src/index.ts +2 -1
  93. package/dist/package/src/layouts/LayoutClassic/index.vue +3 -1
  94. package/dist/package/src/layouts/LayoutColumns/index.vue +3 -1
  95. package/dist/package/src/layouts/LayoutTransverse/index.vue +3 -1
  96. package/dist/package/src/layouts/LayoutVertical/index.vue +9 -1
  97. package/dist/package/src/layouts/components/Header/components/Avatar.vue +26 -6
  98. package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +4 -3
  99. package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +4 -3
  100. package/dist/package/src/layouts/components/Header/components/SearchMenu.vue +16 -8
  101. package/dist/package/src/layouts/components/LockScreen.vue +573 -0
  102. package/dist/package/src/layouts/components/Tabs/index.vue +12 -3
  103. package/dist/package/src/layouts/components/ThemeDrawer/index.scss +4 -4
  104. package/dist/package/src/layouts/index.vue +2 -0
  105. package/dist/package/src/layouts/indexAsync.vue +2 -0
  106. package/dist/package/src/locales/README.md +2 -1
  107. package/dist/package/src/locales/en-US.json +51 -3
  108. package/dist/package/src/locales/index.ts +16 -12
  109. package/dist/package/src/locales/ja-JP.json +51 -3
  110. package/dist/package/src/locales/zh-CN.json +51 -3
  111. package/dist/package/src/locales/zh-TW.json +51 -3
  112. package/dist/package/src/modules/index.ts +2 -4
  113. package/dist/package/src/request.ts +3 -0
  114. package/dist/package/src/routers/index.ts +15 -14
  115. package/dist/package/src/rpc/base/v1/login.ts +37 -4
  116. package/dist/package/src/rpc/base/v1/mfa.ts +180 -0
  117. package/dist/package/src/rpc/base/v1/oauth.ts +25 -0
  118. package/dist/package/src/rpc/base/v1/oauth_client.ts +33 -0
  119. package/dist/package/src/rpc/system/admin/v1/auth.ts +13 -0
  120. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +0 -112
  121. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +10 -6
  122. package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
  123. package/dist/package/src/security.ts +2 -0
  124. package/dist/package/src/stores/interface/index.ts +20 -1
  125. package/dist/package/src/stores/modules/auth.ts +1 -1
  126. package/dist/package/src/stores/modules/config.ts +2 -2
  127. package/dist/package/src/stores/modules/dict.ts +1 -1
  128. package/dist/package/src/stores/modules/lockScreen.ts +78 -0
  129. package/dist/package/src/stores/modules/user.ts +34 -22
  130. package/dist/package/src/stores/runtime.ts +1 -0
  131. package/dist/package/src/styles/element.scss +35 -4
  132. package/dist/package/src/utils/clipboard.ts +22 -0
  133. package/dist/package/src/utils/passwordCrypto.ts +1 -1
  134. package/dist/package/src/utils/passwordStrength.ts +7 -24
  135. package/dist/package/src/utils/request.ts +175 -40
  136. package/dist/package/src/utils/tenant.ts +1 -1
  137. package/dist/package/src/utils/utils.ts +25 -6
  138. package/dist/package/src/utils/webauthn.ts +90 -0
  139. package/dist/package/src/views/login/components/LoginForm.vue +257 -24
  140. package/dist/package/src/views/login/index.scss +2 -2
  141. package/dist/package/src/views/login/index.vue +3 -1
  142. package/dist/package/types/generated/auto-imports.d.ts +132 -129
  143. package/dist/package/types/generated/components.d.ts +4 -72
  144. package/package.json +22 -1
  145. package/types/generated/auto-imports.d.ts +132 -129
  146. package/types/generated/components.d.ts +4 -72
  147. package/vite.config.ts +4 -1
@@ -59,15 +59,18 @@
59
59
  </el-form-item>
60
60
  </el-form>
61
61
  <div class="login-btn">
62
- <el-button :icon="CircleClose" round size="large" @click="resetForm(loginFormRef)">{{ t("common.action.reset") }}</el-button>
62
+ <el-button round size="large" @click="resetForm(loginFormRef)">
63
+ <el-icon><CircleClose /></el-icon>
64
+ {{ t("common.action.reset") }}
65
+ </el-button>
63
66
  <el-button
64
- :icon="UserFilled"
65
67
  round
66
68
  size="large"
67
69
  type="primary"
68
70
  :loading="loading || oauthTicketLoading"
69
71
  @click="handleLogin(loginFormRef)"
70
72
  >
73
+ <el-icon><UserFilled /></el-icon>
71
74
  {{ t("common.action.login") }}
72
75
  </el-button>
73
76
  </div>
@@ -96,11 +99,12 @@
96
99
  </el-tooltip>
97
100
  </div>
98
101
  </div>
99
- <el-dialog
102
+ <ProDialog
100
103
  v-model="behaviorDialogVisible"
101
104
  width="364px"
102
105
  top="16vh"
103
106
  :show-close="false"
107
+ :show-footer="false"
104
108
  append-to-body
105
109
  class="behavior-captcha-dialog"
106
110
  >
@@ -127,7 +131,49 @@
127
131
  :events="rotateCaptchaEvents"
128
132
  />
129
133
  </div>
130
- </el-dialog>
134
+ </ProDialog>
135
+ <ProDialog v-model="mfaDialogVisible" :title="t('core.login.mfa_title')" width="360px" :close-on-click-modal="false">
136
+ <ProForm
137
+ ref="mfaLoginFormRef"
138
+ :model="mfaLoginForm"
139
+ :fields="mfaLoginFormFields"
140
+ :rules="mfaLoginFormRules"
141
+ size="default"
142
+ label-width="auto"
143
+ @keyup.enter.prevent="verifyMfaLogin"
144
+ />
145
+ <template #footer>
146
+ <el-button @click="mfaDialogVisible = false">{{ t("common.action.cancel") }}</el-button>
147
+ <el-button type="primary" :loading="mfaLoading" @click="verifyMfaLogin">
148
+ {{ mfaMethod === "webauthn" ? t("core.login.mfa_webauthn_action") : t("common.action.confirm") }}
149
+ </el-button>
150
+ </template>
151
+ </ProDialog>
152
+ <ProDialog v-model="mfaSetupDialogVisible" :title="t('core.login.mfa_setup_title')" width="520px" :close-on-click-modal="false">
153
+ <template v-if="mfaSetupMethod !== 'webauthn'">
154
+ <MfaSetupPanel :uri="mfaSetupUri" />
155
+ <ProForm
156
+ ref="mfaSetupFormRef"
157
+ :model="mfaSetupForm"
158
+ :fields="mfaSetupFormFields"
159
+ :rules="mfaSetupFormRules"
160
+ size="default"
161
+ label-width="auto"
162
+ @keyup.enter.prevent="confirmMfaSetup"
163
+ />
164
+ </template>
165
+ <template #footer>
166
+ <el-button :disabled="mfaLoading" @click="mfaSetupDialogVisible = false">{{ t("common.action.cancel") }}</el-button>
167
+ <el-button type="primary" :loading="mfaLoading" @click="confirmMfaSetup">
168
+ {{ mfaSetupMethod === "webauthn" ? t("core.login.mfa_webauthn_action") : t("common.action.confirm") }}
169
+ </el-button>
170
+ </template>
171
+ </ProDialog>
172
+ <MfaRecoveryCodesDialog
173
+ v-model="recoveryCodesDialogVisible"
174
+ :codes="recoveryCodes"
175
+ @confirm="finishMfaEnrollment"
176
+ />
131
177
  </template>
132
178
 
133
179
  <script setup lang="ts">
@@ -135,10 +181,16 @@ import { computed, ref, reactive, onMounted, watch } from "vue";
135
181
  import { useRoute, useRouter } from "vue-router";
136
182
  import { HOME_URL } from "../../../config";
137
183
  import { getTimeState } from "../../../utils";
138
- import { defLoginService } from "../../../api/base/login";
139
- import { defOauthService } from "../../../api/base/oauth";
140
- import { ElMessage, ElNotification } from "element-plus";
141
- import type { LoginRequest } from "../../../rpc/base/v1/login";
184
+ import { defLoginService } from "../../../api/base/v1/login";
185
+ import { defMfaService } from "../../../api/base/v1/mfa";
186
+ import { defOauthService } from "../../../api/base/v1/oauth";
187
+ import { createWebAuthnCredential, getWebAuthnAssertion } from "../../../security";
188
+ import ProDialog from "../../../components/Dialog/ProDialog.vue";
189
+ import ProForm from "../../../components/ProForm/index.vue";
190
+ import MfaSetupPanel from "../../../components/Mfa/MfaSetupPanel.vue";
191
+ import MfaRecoveryCodesDialog from "../../../components/Mfa/MfaRecoveryCodesDialog.vue";
192
+ import type { ProFormField, ProFormInstance } from "../../../components/ProForm/interface";
193
+ import { LoginStatus, type LoginRequest, type LoginResponse } from "../../../rpc/base/v1/login";
142
194
  import type { OauthProvider } from "../../../rpc/base/v1/oauth";
143
195
  import { getOauthProviderIcon, withOauthProviderDisplay, type OauthProviderDisplay } from "../../../utils/oauthProvider";
144
196
  import { useUserStore } from "../../../stores/modules/user";
@@ -147,13 +199,13 @@ import { useTabsStore } from "../../../stores/modules/tabs";
147
199
  import { useKeepAliveStore } from "../../../stores/modules/keepAlive";
148
200
  import { initDynamicRouter } from "../../../routers/modules/dynamicRouter";
149
201
  import { isUnmatchedRoute, navigateTo, resolveFrontendRouteURL } from "../../../utils/router";
150
- import { CircleClose, UserFilled } from "@element-plus/icons-vue";
151
- import type { ElForm } from "element-plus";
202
+ import type { ElForm, FormRules } from "element-plus";
152
203
  import { PASSWORD_CRYPTO_SCENE, encryptPassword } from "../../../utils/passwordCrypto";
153
204
  import { useConfigStore } from "../../../stores/modules/config";
154
205
  import { Click as GoCaptchaClick, Rotate as GoCaptchaRotate, Slide as GoCaptchaSlide } from "go-captcha-vue";
155
206
  import "go-captcha-vue/dist/style.css";
156
207
  import { useLocaleStore } from "../../../locales";
208
+ import { clearPasswordChangeRequired, handlePasswordChangeRequired } from "../../../utils/request";
157
209
 
158
210
  const router = useRouter();
159
211
  const route = useRoute();
@@ -182,6 +234,84 @@ const loginRules = computed(() => ({
182
234
  const loading = ref(false);
183
235
  const oauthLoadingProvider = ref("");
184
236
  const oauthTicketLoading = ref(false);
237
+ const mfaDialogVisible = ref(false);
238
+ const mfaLoading = ref(false);
239
+ const mfaChallengeId = ref("");
240
+ const mfaLoginFormRef = ref<ProFormInstance>();
241
+ const mfaLoginForm = reactive({ code: "", recoveryCode: "" });
242
+ const mfaMethod = ref("totp");
243
+ const mfaWebAuthnOptionsJson = ref("");
244
+ const mfaSetupDialogVisible = ref(false);
245
+ const mfaSetupTicket = ref("");
246
+ const mfaSetupUri = ref("");
247
+ const mfaSetupFormRef = ref<ProFormInstance>();
248
+ const mfaSetupForm = reactive({ code: "" });
249
+ const recoveryCodesDialogVisible = ref(false);
250
+ const recoveryCodes = ref<string[]>([]);
251
+ const mfaSetupMethod = ref("totp");
252
+ const mfaSetupWebAuthnOptionsJson = ref("");
253
+
254
+ /** 登录阶段 MFA 输入字段,验证码和恢复码至少填写其一。 */
255
+ const mfaLoginFormFields = computed<ProFormField[]>(() => {
256
+ const recoveryCodeField: ProFormField = {
257
+ prop: "recoveryCode",
258
+ label: t("core.login.mfa_recovery_code"),
259
+ component: "input",
260
+ props: {
261
+ autocomplete: "one-time-code",
262
+ placeholder: t("core.login.mfa_recovery_code")
263
+ }
264
+ };
265
+ if (mfaMethod.value === "webauthn") return [recoveryCodeField];
266
+ return [
267
+ {
268
+ prop: "code",
269
+ label: t("core.login.mfa_code"),
270
+ component: "input",
271
+ props: {
272
+ autocomplete: "one-time-code",
273
+ inputmode: "numeric",
274
+ maxlength: 8,
275
+ placeholder: t("core.login.mfa_code")
276
+ }
277
+ },
278
+ recoveryCodeField
279
+ ];
280
+ });
281
+
282
+ const mfaLoginFormRules = computed<FormRules>(() => {
283
+ if (mfaMethod.value === "webauthn") return {};
284
+ const factorRule = {
285
+ validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
286
+ if (value || mfaLoginForm.code || mfaLoginForm.recoveryCode) {
287
+ callback();
288
+ return;
289
+ }
290
+ callback(new Error(t("core.login.mfa_code")));
291
+ },
292
+ trigger: "blur"
293
+ };
294
+ return { code: [factorRule], recoveryCode: [factorRule] };
295
+ });
296
+
297
+ /** 强制绑定 MFA 的 TOTP 输入字段。 */
298
+ const mfaSetupFormFields = computed<ProFormField[]>(() => [
299
+ {
300
+ prop: "code",
301
+ label: t("core.login.mfa_code"),
302
+ component: "input",
303
+ props: {
304
+ autocomplete: "one-time-code",
305
+ inputmode: "numeric",
306
+ maxlength: 8,
307
+ placeholder: t("core.login.mfa_code")
308
+ }
309
+ }
310
+ ]);
311
+
312
+ const mfaSetupFormRules = computed<FormRules>(() => ({
313
+ code: [{ required: true, message: t("core.login.mfa_code"), trigger: "blur" }]
314
+ }));
185
315
 
186
316
  /** 登录页三方登录展示项。 */
187
317
  type LoginOauthProvider = OauthProvider & OauthProviderDisplay;
@@ -412,12 +542,14 @@ const handleOauthLogin = async (provider: LoginOauthProvider) => {
412
542
  };
413
543
 
414
544
  /** 完成登录后的用户信息、字典与动态路由初始化。 */
415
- const finishLogin = async () => {
545
+ const finishLogin = async (mustChangePassword = false) => {
416
546
  // 1.获取用户信息
417
547
  await userStore.getUserInfo();
418
548
 
419
- // 2.预加载字典缓存,避免页面首次渲染时字典值为空
420
- await dictStore.loadDictionaries();
549
+ if (!mustChangePassword) {
550
+ // 2.预加载字典缓存,避免页面首次渲染时字典值为空
551
+ await dictStore.loadDictionaries();
552
+ }
421
553
 
422
554
  // 3.添加动态路由
423
555
  await initDynamicRouter();
@@ -426,7 +558,7 @@ const finishLogin = async () => {
426
558
  tabsStore.setTabs([]);
427
559
  keepAliveStore.setKeepAliveName([]);
428
560
 
429
- // 5.优先跳回登录失效前页面,没有记录时再进入首个可访问页面。
561
+ // 5.无论是否需要改密,都回到登录前页面;强制改密状态由布局中的全局弹窗展示。
430
562
  // 统一走动态路由感知跳转,避免首次登录后目标页面尚未完成挂载时直接进入 404。
431
563
  await navigateTo(router, getLoginRedirectPath());
432
564
  ElNotification({
@@ -437,6 +569,101 @@ const finishLogin = async () => {
437
569
  });
438
570
  };
439
571
 
572
+ /** 处理密码或 OAuth 返回的 MFA 登录状态。 */
573
+ const handleLoginResponse = async (result: LoginResponse) => {
574
+ if (result.status === LoginStatus.LOGIN_STATUS_MFA_REQUIRED) {
575
+ mfaChallengeId.value = result.mfa_challenge_id;
576
+ mfaMethod.value = result.mfa_method || "totp";
577
+ mfaWebAuthnOptionsJson.value = result.mfa_webauthn_options_json || "";
578
+ mfaLoginForm.code = "";
579
+ mfaLoginForm.recoveryCode = "";
580
+ mfaDialogVisible.value = true;
581
+ return;
582
+ }
583
+ if (result.status === LoginStatus.LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED) {
584
+ mfaSetupTicket.value = result.mfa_setup_ticket;
585
+ mfaSetupMethod.value = result.mfa_method || "totp";
586
+ await beginMfaSetup(result.mfa_setup_ticket);
587
+ return;
588
+ }
589
+ const mustChangePassword = result.status === LoginStatus.LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED;
590
+ if (mustChangePassword) {
591
+ handlePasswordChangeRequired(t("core.layout.password_change_required"));
592
+ } else {
593
+ clearPasswordChangeRequired();
594
+ }
595
+ userStore.updateTokenAuth(result.access_token, result.token_type ?? "", result.expires_in);
596
+ await finishLogin(mustChangePassword);
597
+ };
598
+
599
+ /** 校验登录阶段 MFA 并完成登录。 */
600
+ const verifyMfaLogin = async () => {
601
+ if (mfaLoading.value || !mfaChallengeId.value) return;
602
+ if (mfaMethod.value !== "webauthn" && !(await mfaLoginFormRef.value?.validate())) return;
603
+ mfaLoading.value = true;
604
+ try {
605
+ const useRecoveryCode = Boolean(mfaLoginForm.recoveryCode);
606
+ const webauthnResponseJson =
607
+ mfaMethod.value === "webauthn" && !useRecoveryCode ? await getWebAuthnAssertion(mfaWebAuthnOptionsJson.value) : "";
608
+ if (mfaMethod.value !== "webauthn" && !mfaLoginForm.code && !mfaLoginForm.recoveryCode) return;
609
+ const result = await defMfaService.VerifyMfa({
610
+ challenge_id: mfaChallengeId.value,
611
+ code: useRecoveryCode ? "" : mfaLoginForm.code,
612
+ recovery_code: mfaLoginForm.recoveryCode,
613
+ webauthn_response_json: webauthnResponseJson
614
+ });
615
+ mfaDialogVisible.value = false;
616
+ await handleLoginResponse(result);
617
+ } finally {
618
+ mfaLoading.value = false;
619
+ }
620
+ };
621
+
622
+ /** 开始强制绑定 MFA。 */
623
+ const beginMfaSetup = async (setupTicket: string) => {
624
+ mfaLoading.value = true;
625
+ try {
626
+ const result = await defMfaService.BeginMfaEnrollment({ setup_ticket: setupTicket });
627
+ mfaSetupTicket.value = result.setup_ticket;
628
+ mfaSetupUri.value = result.otpauth_uri;
629
+ mfaSetupMethod.value = result.method || "totp";
630
+ mfaSetupWebAuthnOptionsJson.value = result.webauthn_options_json || "";
631
+ mfaSetupForm.code = "";
632
+ mfaSetupDialogVisible.value = true;
633
+ } finally {
634
+ mfaLoading.value = false;
635
+ }
636
+ };
637
+
638
+ /** 关闭恢复码弹窗并结束强制绑定登录流程。 */
639
+ const finishMfaEnrollment = async () => {
640
+ if (!recoveryCodes.value.length) return;
641
+ recoveryCodes.value = [];
642
+ ElMessage.success(t("core.login.mfa_setup_success"));
643
+ await loadPageCaptcha();
644
+ };
645
+
646
+ /** 确认强制绑定 MFA 并要求用户重新登录。 */
647
+ const confirmMfaSetup = async () => {
648
+ if (mfaLoading.value || !mfaSetupTicket.value) return;
649
+ if (mfaSetupMethod.value !== "webauthn" && !(await mfaSetupFormRef.value?.validate())) return;
650
+ mfaLoading.value = true;
651
+ try {
652
+ const webauthnResponseJson =
653
+ mfaSetupMethod.value === "webauthn" ? await createWebAuthnCredential(mfaSetupWebAuthnOptionsJson.value) : "";
654
+ const result = await defMfaService.ConfirmMfaEnrollment({
655
+ setup_ticket: mfaSetupTicket.value,
656
+ code: mfaSetupForm.code,
657
+ webauthn_response_json: webauthnResponseJson
658
+ });
659
+ mfaSetupDialogVisible.value = false;
660
+ recoveryCodes.value = result.recovery_codes;
661
+ recoveryCodesDialogVisible.value = true;
662
+ } finally {
663
+ mfaLoading.value = false;
664
+ }
665
+ };
666
+
440
667
  /** 消费 OAuth 回调携带的一次性票据。 */
441
668
  const consumeOauthTicket = async () => {
442
669
  const oauthError = route.query.oauth_error;
@@ -452,8 +679,7 @@ const consumeOauthTicket = async () => {
452
679
  oauthTicketLoading.value = true;
453
680
  try {
454
681
  const result = await defOauthService.ExchangeOauthTicket({ ticket });
455
- userStore.updateTokenAuth(result.access_token, result.refresh_token ?? "", result.token_type ?? "", result.expires_in);
456
- await finishLogin();
682
+ await handleLoginResponse(result);
457
683
  } finally {
458
684
  oauthTicketLoading.value = false;
459
685
  }
@@ -537,10 +763,9 @@ const submitLogin = async (captchaToken: string) => {
537
763
  captcha_code: captchaToken,
538
764
  captcha_id: loginForm.captcha_id
539
765
  };
540
- // 1.执行登录接口
541
- await userStore.login(loginRequest);
542
-
543
- await finishLogin();
766
+ // 1.执行登录接口,挑战态不保存令牌,等待 MFA 完成。
767
+ const result = await userStore.login(loginRequest);
768
+ await handleLoginResponse(result);
544
769
  } catch (_error) {
545
770
  await loadPageCaptcha();
546
771
  } finally {
@@ -638,7 +863,7 @@ watch(
638
863
  height: 40px;
639
864
  cursor: pointer;
640
865
  object-fit: contain;
641
- border-radius: 6px;
866
+ border-radius: var(--admin-page-radius);
642
867
  }
643
868
  .behavior-captcha-body {
644
869
  min-height: 0;
@@ -748,15 +973,19 @@ watch(
748
973
  --go-captcha-theme-dot-color-color: #ffffff;
749
974
  --go-captcha-theme-dot-bg-color: color-mix(in srgb, var(--el-color-primary) 68%, transparent);
750
975
  --go-captcha-theme-dot-border-color: var(--el-bg-color);
751
- border-radius: 10px;
976
+ border-radius: var(--admin-page-radius);
752
977
  box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px;
753
978
  }
754
979
 
980
+ :global(.behavior-captcha-dialog .go-captcha.gc-theme) {
981
+ border-radius: var(--admin-page-radius);
982
+ }
983
+
755
984
  :global(.behavior-captcha-dialog .el-dialog__header) {
756
985
  display: none;
757
986
  }
758
987
 
759
- :global(.behavior-captcha-dialog .el-dialog__body) {
988
+ :global(.behavior-captcha-dialog.pro-dialog .el-dialog__body) {
760
989
  padding: 10px 12px 12px;
761
990
  }
762
991
 
@@ -778,7 +1007,11 @@ watch(
778
1007
 
779
1008
  :global(.behavior-captcha-dialog .go-captcha .gc-body) {
780
1009
  margin-top: 0;
781
- border-radius: 6px;
1010
+ border-radius: var(--admin-page-radius);
1011
+ }
1012
+
1013
+ :global(.behavior-captcha-dialog .go-captcha .gc-button-block button) {
1014
+ border-radius: var(--admin-page-radius);
782
1015
  }
783
1016
 
784
1017
  :global(.behavior-captcha-dialog .go-captcha .gc-footer) {
@@ -15,7 +15,7 @@
15
15
  height: 94%;
16
16
  padding: 0 50px;
17
17
  background-color: rgb(255 255 255 / 80%);
18
- border-radius: 10px;
18
+ border-radius: var(--admin-page-radius);
19
19
  .dark {
20
20
  position: absolute;
21
21
  top: 13px;
@@ -41,7 +41,7 @@
41
41
  max-width: 100%;
42
42
  padding: 50px 40px 45px;
43
43
  background-color: var(--el-bg-color);
44
- border-radius: 10px;
44
+ border-radius: var(--admin-page-radius);
45
45
  box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px;
46
46
  .login-logo {
47
47
  display: flex;
@@ -4,7 +4,7 @@
4
4
  <SwitchDark class="dark" />
5
5
  <LocaleSwitch class="locale" />
6
6
  <div class="login-left">
7
- <img class="login-left-img" :src="backgroundUrl" alt="login" />
7
+ <img class="login-left-img" :src="backgroundUrl" :alt="t('core.login.background_alt')" />
8
8
  </div>
9
9
  <div class="login-form">
10
10
  <div ref="loginLogoRef" class="login-logo">
@@ -23,8 +23,10 @@ import LoginForm from "./components/LoginForm.vue";
23
23
  import SwitchDark from "../../components/SwitchDark/index.vue";
24
24
  import LocaleSwitch from "../../components/LocaleSwitch/index.vue";
25
25
  import { useConfigStore } from "../../stores/modules/config";
26
+ import { useLocaleStore } from "../../locales";
26
27
 
27
28
  const configStore = useConfigStore();
29
+ const { t } = useLocaleStore();
28
30
  const projectName = computed(() => configStore.display.sysName || import.meta.env.VITE_GLOB_APP_TITLE);
29
31
  const logoUrl = computed(() => configStore.display.adminLogo);
30
32
  const backgroundUrl = computed(() => configStore.display.background);