@liujitcn/kratos-admin-system 0.0.29 → 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 +18 -1
- package/dist/package/declarations/src/api/base/notification.d.ts +24 -0
- package/dist/package/declarations/src/api/system/base_audit_log.d.ts +55 -0
- package/dist/package/declarations/src/api/system/base_dashboard.d.ts +14 -0
- package/dist/package/declarations/src/api/system/base_file.d.ts +13 -0
- package/dist/package/declarations/src/api/system/base_login_policy.d.ts +10 -0
- package/dist/package/declarations/src/api/system/base_message.d.ts +24 -0
- package/dist/package/declarations/src/api/system/base_message_category.d.ts +21 -0
- package/dist/package/declarations/src/api/system/base_session.d.ts +11 -0
- package/dist/package/declarations/src/api/system/oauth_client.d.ts +24 -0
- package/dist/package/declarations/src/components/AuditLogTable.vue.d.ts +45 -0
- package/dist/package/declarations/src/components/auditLog.d.ts +9 -0
- package/dist/package/declarations/src/locales/generated.d.ts +1312 -16
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +149 -0
- package/dist/package/declarations/src/rpc/base/v1/notification.d.ts +281 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +25 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth_client.d.ts +23 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_audit_log.d.ts +640 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_dashboard.d.ts +60 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_file.d.ts +73 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +10 -6
- package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +5 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_login_policy.d.ts +51 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message.d.ts +193 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_message_category.d.ts +125 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_session.d.ts +33 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +3 -3
- package/dist/package/declarations/src/rpc/system/admin/v1/oauth_client.d.ts +145 -0
- package/dist/package/declarations/src/views/base/api-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/dashboard/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/data-access-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/file/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/login-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/login-policy/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/message/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/message-category/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/oauth-client/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/operation-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/permission-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/base/policy-evaluation-log/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/message/inbox/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/profile/components/session.vue.d.ts +3 -0
- package/dist/package/src/api/base/notification.ts +69 -0
- package/dist/package/src/api/base/sse.ts +4 -4
- package/dist/package/src/api/system/auth.ts +1 -0
- package/dist/package/src/api/system/base_audit_log.ts +168 -0
- package/dist/package/src/api/system/base_dashboard.ts +55 -0
- package/dist/package/src/api/system/base_file.ts +43 -0
- package/dist/package/src/api/system/base_login_policy.ts +32 -0
- package/dist/package/src/api/system/base_message.ts +70 -0
- package/dist/package/src/api/system/base_message_category.ts +57 -0
- package/dist/package/src/api/system/base_session.ts +33 -0
- package/dist/package/src/api/system/oauth_client.ts +102 -0
- package/dist/package/src/components/AuditLogTable.vue +105 -0
- package/dist/package/src/components/Notification.vue +196 -0
- package/dist/package/src/components/auditLog.ts +46 -0
- package/dist/package/src/locales/README.md +4 -2
- package/dist/package/src/locales/en-US.json +334 -10
- package/dist/package/src/locales/ja-JP.json +338 -14
- package/dist/package/src/locales/zh-CN.json +334 -10
- package/dist/package/src/locales/zh-TW.json +337 -13
- package/dist/package/src/module.ts +5 -2
- package/dist/package/src/rpc/base/v1/login.ts +172 -0
- package/dist/package/src/rpc/base/v1/notification.ts +315 -0
- package/dist/package/src/rpc/base/v1/oauth.ts +25 -0
- package/dist/package/src/rpc/base/v1/oauth_client.ts +33 -0
- package/dist/package/src/rpc/system/admin/v1/base_audit_log.ts +737 -0
- package/dist/package/src/rpc/system/admin/v1/base_dashboard.ts +81 -0
- package/dist/package/src/rpc/system/admin/v1/base_file.ts +88 -0
- package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +10 -6
- package/dist/package/src/rpc/system/admin/v1/base_job.ts +5 -0
- package/dist/package/src/rpc/system/admin/v1/base_login_policy.ts +63 -0
- package/dist/package/src/rpc/system/admin/v1/base_message.ts +236 -0
- package/dist/package/src/rpc/system/admin/v1/base_message_category.ts +149 -0
- package/dist/package/src/rpc/system/admin/v1/base_session.ts +44 -0
- package/dist/package/src/rpc/system/admin/v1/base_user.ts +3 -3
- package/dist/package/src/rpc/system/admin/v1/oauth_client.ts +174 -0
- package/dist/package/src/views/ai/chat/components/ChatPanel.vue +2 -2
- package/dist/package/src/views/ai/chat/message.ts +7 -3
- package/dist/package/src/views/base/api-log/index.vue +83 -0
- package/dist/package/src/views/base/config/index.vue +3 -3
- package/dist/package/src/views/base/dashboard/index.vue +200 -0
- package/dist/package/src/views/base/data-access-log/index.vue +84 -0
- package/dist/package/src/views/base/dict/index.vue +2 -2
- package/dist/package/src/views/base/dict/item.vue +2 -2
- package/dist/package/src/views/base/file/index.vue +119 -0
- package/dist/package/src/views/base/job/index.vue +62 -7
- package/dist/package/src/views/base/language/index.vue +2 -1
- package/dist/package/src/views/base/login-log/index.vue +92 -0
- package/dist/package/src/views/base/login-policy/index.vue +219 -0
- package/dist/package/src/views/base/menu/index.vue +2 -2
- package/dist/package/src/views/base/message/index.vue +582 -0
- package/dist/package/src/views/base/message-category/index.vue +356 -0
- package/dist/package/src/views/base/oauth-client/index.vue +411 -0
- package/dist/package/src/views/base/operation-log/index.vue +82 -0
- package/dist/package/src/views/base/permission-log/index.vue +85 -0
- package/dist/package/src/views/base/policy-evaluation-log/index.vue +82 -0
- package/dist/package/src/views/message/inbox/index.vue +249 -0
- package/dist/package/src/views/profile/components/base.vue +6 -5
- package/dist/package/src/views/profile/components/password.vue +2 -2
- package/dist/package/src/views/profile/components/security.vue +475 -62
- package/dist/package/src/views/profile/components/session.vue +153 -0
- package/dist/package/src/views/profile/index.vue +35 -22
- package/dist/package/src/views/tool/code-gen/config.ts +1 -0
- package/dist/package/src/views/tool/ops-monitoring/index.vue +25 -21
- package/package.json +2 -2
- package/dist/package/declarations/src/api/system/base_log.d.ts +0 -9
- package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +0 -87
- package/dist/package/src/api/system/base_log.ts +0 -32
- package/dist/package/src/rpc/system/admin/v1/base_log.ts +0 -101
- package/dist/package/src/views/base/log/index.vue +0 -339
- /package/dist/package/declarations/src/{views/base/log/index.vue.d.ts → components/Notification.vue.d.ts} +0 -0
|
@@ -27,6 +27,34 @@
|
|
|
27
27
|
{{ profile.phone ? t("system.profile.security.action.change_phone") : t("system.profile.security.action.bind_now") }}
|
|
28
28
|
</el-button>
|
|
29
29
|
</div>
|
|
30
|
+
<div class="security-item">
|
|
31
|
+
<div class="security-item__content">
|
|
32
|
+
<strong>{{ t("system.profile.security.field.mfa") }}</strong>
|
|
33
|
+
<p>
|
|
34
|
+
{{
|
|
35
|
+
mfaEnabled
|
|
36
|
+
? t("system.profile.security.message.mfa_enabled_method", { method: mfaMethodLabel })
|
|
37
|
+
: t("system.profile.security.message.mfa_disabled")
|
|
38
|
+
}}
|
|
39
|
+
</p>
|
|
40
|
+
<p v-if="mfaEnabled && mfaMethod === 'webauthn'" class="security-item__hint">
|
|
41
|
+
{{ t("system.profile.security.message.mfa_webauthn_manage_hint") }}
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="security-item__actions">
|
|
45
|
+
<el-button v-if="!mfaEnabled" plain @click="openMfaSetup">{{
|
|
46
|
+
t("system.profile.security.action.mfa_enable")
|
|
47
|
+
}}</el-button>
|
|
48
|
+
<template v-else>
|
|
49
|
+
<el-button v-if="mfaMethod === 'totp'" plain @click="openMfaRecovery">
|
|
50
|
+
{{ t("system.profile.security.action.mfa_recovery") }}
|
|
51
|
+
</el-button>
|
|
52
|
+
<el-button plain type="danger" @click="openMfaDisable">{{
|
|
53
|
+
t("system.profile.security.action.mfa_disable")
|
|
54
|
+
}}</el-button>
|
|
55
|
+
</template>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
30
58
|
<div v-for="item in oauthBindings" :key="item.provider" class="security-item">
|
|
31
59
|
<div class="security-item__content security-item__content--oauth">
|
|
32
60
|
<el-tooltip :content="oauthName(item)" placement="top" :trigger="['hover', 'focus']">
|
|
@@ -38,7 +66,9 @@
|
|
|
38
66
|
<strong>{{ oauthName(item) }}</strong>
|
|
39
67
|
<p>
|
|
40
68
|
{{
|
|
41
|
-
item.bound
|
|
69
|
+
item.bound
|
|
70
|
+
? t("system.profile.security.message.oauth_bound")
|
|
71
|
+
: t("system.profile.security.message.oauth_unbound")
|
|
42
72
|
}}
|
|
43
73
|
</p>
|
|
44
74
|
</div>
|
|
@@ -59,27 +89,6 @@
|
|
|
59
89
|
</div>
|
|
60
90
|
</el-card>
|
|
61
91
|
|
|
62
|
-
<el-card class="status-card" shadow="never">
|
|
63
|
-
<template #header>
|
|
64
|
-
<div class="status-header">
|
|
65
|
-
<div>
|
|
66
|
-
<h3>{{ t("system.profile.security.status.title") }}</h3>
|
|
67
|
-
<p>{{ t("system.profile.security.status.description") }}</p>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</template>
|
|
71
|
-
<div class="status-grid">
|
|
72
|
-
<div class="status-item">
|
|
73
|
-
<span>{{ t("system.profile.security.field.phone_verification") }}</span>
|
|
74
|
-
<strong>{{ profile.phone ? t("common.status.enabled") : t("common.status.disabled") }}</strong>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="status-item">
|
|
77
|
-
<span>{{ t("system.profile.security.field.profile_completion") }}</span>
|
|
78
|
-
<strong>{{ profileCompletion }}</strong>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</el-card>
|
|
82
|
-
|
|
83
92
|
<ProDialog
|
|
84
93
|
v-model="phoneDialogVisible"
|
|
85
94
|
:title="t('system.profile.security.dialog.bind_phone')"
|
|
@@ -110,6 +119,58 @@
|
|
|
110
119
|
</div>
|
|
111
120
|
</template>
|
|
112
121
|
</ProDialog>
|
|
122
|
+
<ProDialog
|
|
123
|
+
v-model="mfaDialogVisible"
|
|
124
|
+
:title="mfaDialogTitle"
|
|
125
|
+
:width="'min(560px, calc(100vw - 32px))'"
|
|
126
|
+
@closed="resetMfaDialog"
|
|
127
|
+
>
|
|
128
|
+
<div class="mfa-dialog-content">
|
|
129
|
+
<MfaSetupPanel v-if="mfaSetupUri" :uri="mfaSetupUri" />
|
|
130
|
+
<ProForm
|
|
131
|
+
ref="mfaFormRef"
|
|
132
|
+
class="mfa-dialog-form"
|
|
133
|
+
:model="mfaForm"
|
|
134
|
+
:fields="mfaDialogFields"
|
|
135
|
+
:rules="mfaDialogRules"
|
|
136
|
+
label-width="auto"
|
|
137
|
+
autocomplete="off"
|
|
138
|
+
size="default"
|
|
139
|
+
/>
|
|
140
|
+
<div v-if="recoveryCodes.length" class="mfa-dialog-recovery">
|
|
141
|
+
<div class="mfa-copyable-value">
|
|
142
|
+
<span class="mfa-copyable-value__text">{{ recoveryCodesText }}</span>
|
|
143
|
+
<el-tooltip :content="t('system.profile.security.action.mfa_copy_recovery_codes')" placement="top">
|
|
144
|
+
<el-button
|
|
145
|
+
class="mfa-copy-button"
|
|
146
|
+
text
|
|
147
|
+
circle
|
|
148
|
+
:aria-label="t('system.profile.security.action.mfa_copy_recovery_codes')"
|
|
149
|
+
@click="copyRecoveryCodes"
|
|
150
|
+
>
|
|
151
|
+
<el-icon class="mfa-copy-button__icon"><CopyDocument /></el-icon>
|
|
152
|
+
</el-button>
|
|
153
|
+
</el-tooltip>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<p v-if="recoveryCodes.length" class="mfa-recovery-hint">
|
|
157
|
+
{{ t("system.profile.security.message.mfa_recovery_codes_warning") }}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
<template #footer>
|
|
161
|
+
<div class="dialog-footer">
|
|
162
|
+
<el-button @click="mfaDialogVisible = false">{{ t("common.action.close") }}</el-button>
|
|
163
|
+
<el-button :type="mfaDialogActionType" :loading="mfaLoading" @click="handleMfaAction">
|
|
164
|
+
{{ mfaDialogActionText }}
|
|
165
|
+
</el-button>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</ProDialog>
|
|
169
|
+
<MfaRecoveryCodesDialog
|
|
170
|
+
v-model="recoveryCodesDialogVisible"
|
|
171
|
+
:codes="recoveryCodes"
|
|
172
|
+
@confirm="handleRecoveryCodesClosed"
|
|
173
|
+
/>
|
|
113
174
|
</div>
|
|
114
175
|
</template>
|
|
115
176
|
|
|
@@ -118,6 +179,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
|
|
118
179
|
import { useRoute, useRouter } from "vue-router";
|
|
119
180
|
import { t } from "@liujitcn/kratos-admin-core";
|
|
120
181
|
import { defProfileAuthService } from "@liujitcn/kratos-admin-system/api/system/auth";
|
|
182
|
+
import { defMfaService } from "@liujitcn/kratos-admin-core/api/base/mfa";
|
|
121
183
|
import { defProfileOauthService } from "@liujitcn/kratos-admin-system/api/base/oauth";
|
|
122
184
|
import type { OauthBinding } from "@liujitcn/kratos-admin-system/rpc/base/v1/oauth";
|
|
123
185
|
import type {
|
|
@@ -127,9 +189,22 @@ import type {
|
|
|
127
189
|
} from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/auth";
|
|
128
190
|
import ProDialog from "@liujitcn/kratos-admin-core/components/Dialog/ProDialog.vue";
|
|
129
191
|
import ProForm from "@liujitcn/kratos-admin-core/components/ProForm/index.vue";
|
|
192
|
+
import MfaSetupPanel from "@liujitcn/kratos-admin-core/components/Mfa/MfaSetupPanel.vue";
|
|
193
|
+
import MfaRecoveryCodesDialog from "@liujitcn/kratos-admin-core/components/Mfa/MfaRecoveryCodesDialog.vue";
|
|
130
194
|
import type { ProFormField, ProFormInstance } from "@liujitcn/kratos-admin-core/components/ProForm/interface";
|
|
131
|
-
import {
|
|
195
|
+
import type { PasswordCrypto } from "@liujitcn/kratos-admin-core/rpc/common/v1/types";
|
|
196
|
+
import {
|
|
197
|
+
getOauthProviderIcon,
|
|
198
|
+
withOauthProviderDisplay,
|
|
199
|
+
type OauthProviderDisplay,
|
|
200
|
+
PASSWORD_CRYPTO_SCENE,
|
|
201
|
+
encryptPassword,
|
|
202
|
+
copyText,
|
|
203
|
+
createWebAuthnCredential,
|
|
204
|
+
getWebAuthnAssertion
|
|
205
|
+
} from "@liujitcn/kratos-admin-core/security";
|
|
132
206
|
import { resolveFrontendRouteURL } from "@liujitcn/kratos-admin-core/navigation";
|
|
207
|
+
import { useUserStore } from "@liujitcn/kratos-admin-core/stores/runtime";
|
|
133
208
|
|
|
134
209
|
/** 安全中心组件属性。 */
|
|
135
210
|
interface ProfileSecurityProps {
|
|
@@ -146,7 +221,9 @@ const emit = defineEmits<{
|
|
|
146
221
|
|
|
147
222
|
const route = useRoute();
|
|
148
223
|
const router = useRouter();
|
|
224
|
+
const userStore = useUserStore();
|
|
149
225
|
const phoneFormRef = ref<ProFormInstance>();
|
|
226
|
+
const mfaFormRef = ref<ProFormInstance>();
|
|
150
227
|
const phoneDialogVisible = ref(false);
|
|
151
228
|
const submitLoading = ref(false);
|
|
152
229
|
/** 安全中心三方账号绑定展示项。 */
|
|
@@ -163,6 +240,139 @@ const phoneForm = reactive<UserPhoneForm>({
|
|
|
163
240
|
const sendPhoneCodeForm = reactive<SendPhoneCodeRequest>({
|
|
164
241
|
phone: ""
|
|
165
242
|
});
|
|
243
|
+
const mfaEnabled = ref(false);
|
|
244
|
+
const mfaPolicy = ref("");
|
|
245
|
+
const mfaMethod = ref("totp");
|
|
246
|
+
const mfaDialogVisible = ref(false);
|
|
247
|
+
const recoveryCodesDialogVisible = ref(false);
|
|
248
|
+
const mfaDialogMode = ref<"setup" | "disable" | "recovery">("setup");
|
|
249
|
+
const mfaLoading = ref(false);
|
|
250
|
+
/** MFA 操作弹窗表单。 */
|
|
251
|
+
interface MfaDialogForm {
|
|
252
|
+
/** 当前密码,仅用于生成一次性加密请求。 */
|
|
253
|
+
password: string;
|
|
254
|
+
/** 动态口令。 */
|
|
255
|
+
code: string;
|
|
256
|
+
/** 一次性恢复码。 */
|
|
257
|
+
recoveryCode: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const mfaForm = reactive<MfaDialogForm>({
|
|
261
|
+
password: "",
|
|
262
|
+
code: "",
|
|
263
|
+
recoveryCode: ""
|
|
264
|
+
});
|
|
265
|
+
const mfaSetupTicket = ref("");
|
|
266
|
+
const mfaSetupUri = ref("");
|
|
267
|
+
const mfaSetupMethod = ref("totp");
|
|
268
|
+
const mfaSetupWebAuthnOptionsJson = ref("");
|
|
269
|
+
const recoveryCodes = ref<string[]>([]);
|
|
270
|
+
const recoveryCodesText = computed(() => recoveryCodes.value.join("\n"));
|
|
271
|
+
/** 禁用 MFA 时是否需要同时校验已绑定的 MFA 因子。 */
|
|
272
|
+
const mfaFactorEnabled = computed(() => mfaDialogMode.value === "disable" && mfaEnabled.value);
|
|
273
|
+
const mfaDialogTitle = computed(() => {
|
|
274
|
+
if (mfaDialogMode.value === "disable") return t("system.profile.security.dialog.mfa_disable");
|
|
275
|
+
if (mfaDialogMode.value === "recovery") return t("system.profile.security.dialog.mfa_recovery");
|
|
276
|
+
if (!mfaSetupTicket.value) return t("core.password.verify_title");
|
|
277
|
+
return t("system.profile.security.dialog.mfa_setup");
|
|
278
|
+
});
|
|
279
|
+
const mfaDialogActionText = computed(() => {
|
|
280
|
+
if (mfaDialogMode.value === "disable") return t("system.profile.security.action.mfa_disable");
|
|
281
|
+
if (mfaDialogMode.value === "recovery") return t("common.action.confirm");
|
|
282
|
+
if (mfaSetupUri.value || (mfaSetupMethod.value === "webauthn" && mfaSetupTicket.value)) {
|
|
283
|
+
return mfaSetupMethod.value === "webauthn"
|
|
284
|
+
? t("core.login.mfa_webauthn_action")
|
|
285
|
+
: t("system.profile.security.action.mfa_confirm");
|
|
286
|
+
}
|
|
287
|
+
return t("system.profile.security.action.mfa_next");
|
|
288
|
+
});
|
|
289
|
+
const mfaDialogActionType = computed(() => (mfaDialogMode.value === "disable" ? "danger" : "primary"));
|
|
290
|
+
const mfaMethodLabel = computed(() =>
|
|
291
|
+
mfaMethod.value === "webauthn"
|
|
292
|
+
? t("system.profile.security.message.mfa_method_webauthn")
|
|
293
|
+
: t("system.profile.security.message.mfa_method_totp")
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const mfaDialogFields = computed<ProFormField[]>(() => {
|
|
297
|
+
const passwordField: ProFormField = {
|
|
298
|
+
prop: "password",
|
|
299
|
+
label: t("system.profile.security.field.login_password"),
|
|
300
|
+
component: "password",
|
|
301
|
+
props: {
|
|
302
|
+
autocomplete: "current-password",
|
|
303
|
+
name: "mfa-current-password",
|
|
304
|
+
placeholder: t("system.profile.security.placeholder.current_password")
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const codeField: ProFormField = {
|
|
308
|
+
prop: "code",
|
|
309
|
+
label: t("system.profile.security.field.code"),
|
|
310
|
+
component: "input",
|
|
311
|
+
itemProps: { required: mfaDialogMode.value !== "disable" },
|
|
312
|
+
props: {
|
|
313
|
+
autocomplete: "one-time-code",
|
|
314
|
+
inputmode: "numeric",
|
|
315
|
+
maxlength: 8,
|
|
316
|
+
name: "mfa-verification-code",
|
|
317
|
+
placeholder: t("system.profile.security.placeholder.mfa_code")
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const recoveryCodeField: ProFormField = {
|
|
321
|
+
prop: "recoveryCode",
|
|
322
|
+
label: t("system.profile.security.field.mfa_recovery_code"),
|
|
323
|
+
component: "input",
|
|
324
|
+
props: {
|
|
325
|
+
autocomplete: "off",
|
|
326
|
+
name: "mfa-recovery-code",
|
|
327
|
+
placeholder: t("system.profile.security.placeholder.mfa_recovery_code")
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
if (mfaDialogMode.value === "setup" && !mfaSetupUri.value && !mfaSetupTicket.value) return [passwordField];
|
|
331
|
+
if (mfaDialogMode.value === "setup" && mfaSetupMethod.value === "webauthn") return [];
|
|
332
|
+
if (mfaDialogMode.value === "disable" && !mfaFactorEnabled.value) return [passwordField];
|
|
333
|
+
if (mfaDialogMode.value === "disable" && mfaMethod.value === "webauthn") return [passwordField, recoveryCodeField];
|
|
334
|
+
if (mfaDialogMode.value === "disable") return [passwordField, codeField, recoveryCodeField];
|
|
335
|
+
return [codeField];
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const mfaDialogRules = computed(() => {
|
|
339
|
+
if (mfaDialogMode.value === "disable") {
|
|
340
|
+
if (!mfaFactorEnabled.value) {
|
|
341
|
+
return {
|
|
342
|
+
password: [{ required: true, message: t("system.profile.security.placeholder.current_password"), trigger: "blur" }]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (mfaMethod.value === "webauthn") {
|
|
346
|
+
return {
|
|
347
|
+
password: [{ required: true, message: t("system.profile.security.placeholder.current_password"), trigger: "blur" }]
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
password: [{ required: true, message: t("system.profile.security.placeholder.current_password"), trigger: "blur" }],
|
|
352
|
+
code: [
|
|
353
|
+
{
|
|
354
|
+
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
|
355
|
+
if (value || mfaForm.recoveryCode) {
|
|
356
|
+
callback();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
callback(new Error(t("system.profile.security.message.mfa_disable_factor_required")));
|
|
360
|
+
},
|
|
361
|
+
trigger: "blur"
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (mfaDialogMode.value === "setup" && !mfaSetupUri.value && !mfaSetupTicket.value) {
|
|
367
|
+
return {
|
|
368
|
+
password: [{ required: true, message: t("system.profile.security.placeholder.current_password"), trigger: "blur" }]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (mfaDialogMode.value === "setup" && mfaSetupMethod.value === "webauthn") return {};
|
|
372
|
+
return {
|
|
373
|
+
code: [{ required: true, message: t("system.profile.security.placeholder.mfa_code"), trigger: "blur" }]
|
|
374
|
+
};
|
|
375
|
+
});
|
|
166
376
|
|
|
167
377
|
const phoneFormFields = computed<ProFormField[]>(() => [
|
|
168
378
|
{
|
|
@@ -194,13 +404,6 @@ const mobileTip = computed(() => {
|
|
|
194
404
|
: t("system.profile.security.message.phone_unbound");
|
|
195
405
|
});
|
|
196
406
|
|
|
197
|
-
/** 根据关键资料估算当前资料完成度。 */
|
|
198
|
-
const profileCompletion = computed(() => {
|
|
199
|
-
const fieldList = [props.profile.nick_name, props.profile.phone, props.profile.role_name, props.profile.dept_name];
|
|
200
|
-
const completedCount = fieldList.filter(item => Boolean(item)).length;
|
|
201
|
-
return `${Math.round((completedCount / fieldList.length) * 100)}%`;
|
|
202
|
-
});
|
|
203
|
-
|
|
204
407
|
/** 获取当前安全设置页绝对地址,供 OAuth 绑定完成后回跳到前端页面。 */
|
|
205
408
|
function getCurrentSecurityPath() {
|
|
206
409
|
const query = { ...route.query };
|
|
@@ -216,6 +419,185 @@ async function loadOauthBindings() {
|
|
|
216
419
|
oauthBindings.value = result.bindings.map(withOauthProviderDisplay);
|
|
217
420
|
}
|
|
218
421
|
|
|
422
|
+
/** 拉取当前用户 MFA 状态。 */
|
|
423
|
+
async function loadMfaStatus() {
|
|
424
|
+
const result = await defMfaService.GetMfaStatus({});
|
|
425
|
+
mfaEnabled.value = result.enabled;
|
|
426
|
+
mfaPolicy.value = result.policy;
|
|
427
|
+
mfaMethod.value = result.method || "totp";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** 打开 MFA 绑定弹窗。 */
|
|
431
|
+
function openMfaSetup() {
|
|
432
|
+
resetMfaDialog();
|
|
433
|
+
mfaDialogMode.value = "setup";
|
|
434
|
+
mfaDialogVisible.value = true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** 打开 MFA 禁用弹窗。 */
|
|
438
|
+
function openMfaDisable() {
|
|
439
|
+
resetMfaDialog();
|
|
440
|
+
mfaDialogMode.value = "disable";
|
|
441
|
+
mfaDialogVisible.value = true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 打开恢复码重新生成弹窗。 */
|
|
445
|
+
function openMfaRecovery() {
|
|
446
|
+
resetMfaDialog();
|
|
447
|
+
mfaDialogMode.value = "recovery";
|
|
448
|
+
mfaDialogVisible.value = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** 开始绑定 MFA。 */
|
|
452
|
+
async function beginMfaSetup(password: PasswordCrypto) {
|
|
453
|
+
mfaLoading.value = true;
|
|
454
|
+
try {
|
|
455
|
+
const result = await defMfaService.BeginMfaSetup({ password, setup_ticket: "" });
|
|
456
|
+
mfaSetupTicket.value = result.setup_ticket;
|
|
457
|
+
mfaSetupUri.value = result.otpauth_uri;
|
|
458
|
+
mfaSetupMethod.value = result.method || mfaMethod.value || "totp";
|
|
459
|
+
mfaSetupWebAuthnOptionsJson.value = result.webauthn_options_json || "";
|
|
460
|
+
} finally {
|
|
461
|
+
mfaLoading.value = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** 确认绑定 MFA。 */
|
|
466
|
+
async function confirmMfaSetup() {
|
|
467
|
+
if (!mfaSetupTicket.value || (mfaSetupMethod.value !== "webauthn" && !mfaForm.code)) return;
|
|
468
|
+
mfaLoading.value = true;
|
|
469
|
+
try {
|
|
470
|
+
const webauthnResponseJson =
|
|
471
|
+
mfaSetupMethod.value === "webauthn" ? await createWebAuthnCredential(mfaSetupWebAuthnOptionsJson.value) : "";
|
|
472
|
+
const result = await defMfaService.ConfirmMfaSetup({
|
|
473
|
+
setup_ticket: mfaSetupTicket.value,
|
|
474
|
+
code: mfaForm.code,
|
|
475
|
+
webauthn_response_json: webauthnResponseJson
|
|
476
|
+
});
|
|
477
|
+
recoveryCodes.value = result.recovery_codes;
|
|
478
|
+
mfaEnabled.value = true;
|
|
479
|
+
mfaDialogVisible.value = false;
|
|
480
|
+
recoveryCodesDialogVisible.value = true;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
ElMessage.error(error instanceof Error ? error.message : t("common.message.request_error"));
|
|
483
|
+
} finally {
|
|
484
|
+
mfaLoading.value = false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** 禁用 MFA。 */
|
|
489
|
+
async function disableMfa() {
|
|
490
|
+
if (!mfaForm.password) return;
|
|
491
|
+
if (mfaFactorEnabled.value && mfaMethod.value === "totp" && !mfaForm.code && !mfaForm.recoveryCode) {
|
|
492
|
+
ElMessage.error(t("system.profile.security.message.mfa_disable_factor_required"));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
mfaLoading.value = true;
|
|
496
|
+
try {
|
|
497
|
+
const password = await encryptPassword(mfaForm.password, PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA);
|
|
498
|
+
if (mfaForm.recoveryCode) {
|
|
499
|
+
await defMfaService.DisableMfa({
|
|
500
|
+
password,
|
|
501
|
+
code: "",
|
|
502
|
+
webauthn_challenge_id: "",
|
|
503
|
+
webauthn_response_json: "",
|
|
504
|
+
recovery_code: mfaForm.recoveryCode
|
|
505
|
+
});
|
|
506
|
+
} else if (mfaMethod.value === "webauthn") {
|
|
507
|
+
const challenge = await defMfaService.BeginMfaDisable({});
|
|
508
|
+
const webauthnResponseJson = await getWebAuthnAssertion(challenge.webauthn_options_json);
|
|
509
|
+
await defMfaService.DisableMfa({
|
|
510
|
+
password,
|
|
511
|
+
code: "",
|
|
512
|
+
webauthn_challenge_id: challenge.challenge_id,
|
|
513
|
+
webauthn_response_json: webauthnResponseJson,
|
|
514
|
+
recovery_code: ""
|
|
515
|
+
});
|
|
516
|
+
} else {
|
|
517
|
+
await defMfaService.DisableMfa({
|
|
518
|
+
password,
|
|
519
|
+
code: mfaForm.code,
|
|
520
|
+
webauthn_challenge_id: "",
|
|
521
|
+
webauthn_response_json: "",
|
|
522
|
+
recovery_code: ""
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
mfaEnabled.value = false;
|
|
526
|
+
ElMessage.success(t("system.profile.security.message.mfa_disabled"));
|
|
527
|
+
await userStore.clearAuthData();
|
|
528
|
+
await router.replace({ path: "/login" });
|
|
529
|
+
} finally {
|
|
530
|
+
mfaLoading.value = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** 重新生成恢复码。 */
|
|
535
|
+
async function regenerateRecoveryCodes() {
|
|
536
|
+
if (!mfaForm.code) return;
|
|
537
|
+
mfaLoading.value = true;
|
|
538
|
+
try {
|
|
539
|
+
const result = await defMfaService.RegenerateMfaRecoveryCodes({ code: mfaForm.code });
|
|
540
|
+
recoveryCodes.value = result.recovery_codes;
|
|
541
|
+
} finally {
|
|
542
|
+
mfaLoading.value = false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 复制当前显示的 MFA 恢复码。 */
|
|
547
|
+
async function copyRecoveryCodes() {
|
|
548
|
+
await copyText(recoveryCodesText.value);
|
|
549
|
+
ElMessage.success(t("system.profile.security.message.mfa_recovery_codes_copied"));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** 关闭绑定成功后的恢复码弹窗并重新登录。 */
|
|
553
|
+
async function handleRecoveryCodesClosed() {
|
|
554
|
+
if (!recoveryCodes.value.length) return;
|
|
555
|
+
ElMessage.success(t("system.profile.security.message.mfa_enabled"));
|
|
556
|
+
recoveryCodes.value = [];
|
|
557
|
+
await userStore.clearAuthData();
|
|
558
|
+
await router.replace({ path: "/login" });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** 校验当前 MFA 表单并执行对应步骤。 */
|
|
562
|
+
async function handleMfaAction() {
|
|
563
|
+
if (!(await mfaFormRef.value?.validate())) return;
|
|
564
|
+
if (mfaDialogMode.value === "disable") {
|
|
565
|
+
await disableMfa();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (mfaDialogMode.value === "recovery") {
|
|
569
|
+
await regenerateRecoveryCodes();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (mfaSetupUri.value || (mfaSetupMethod.value === "webauthn" && mfaSetupTicket.value)) {
|
|
573
|
+
await confirmMfaSetup();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const password = await encryptPassword(mfaForm.password, PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA);
|
|
578
|
+
await beginMfaSetup(password);
|
|
579
|
+
mfaForm.password = "";
|
|
580
|
+
} catch (error) {
|
|
581
|
+
ElMessage.error(error instanceof Error ? error.message : t("common.message.request_error"));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** 重置 MFA 弹窗状态。 */
|
|
586
|
+
function resetMfaDialog() {
|
|
587
|
+
mfaForm.password = "";
|
|
588
|
+
mfaForm.code = "";
|
|
589
|
+
mfaForm.recoveryCode = "";
|
|
590
|
+
mfaFormRef.value?.clearValidate();
|
|
591
|
+
mfaSetupTicket.value = "";
|
|
592
|
+
mfaSetupUri.value = "";
|
|
593
|
+
mfaSetupMethod.value = "totp";
|
|
594
|
+
mfaSetupWebAuthnOptionsJson.value = "";
|
|
595
|
+
// 绑定弹窗关闭与恢复码弹窗打开同时进行时,保留本次绑定返回的恢复码。
|
|
596
|
+
if (!recoveryCodesDialogVisible.value) {
|
|
597
|
+
recoveryCodes.value = [];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
219
601
|
/** 根据当前语言返回三方登录方式名称。 */
|
|
220
602
|
function oauthName(binding: SecurityOauthBinding) {
|
|
221
603
|
return binding.nameKey.includes(".") ? t(binding.nameKey) : binding.nameKey;
|
|
@@ -357,6 +739,7 @@ onBeforeUnmount(() => {
|
|
|
357
739
|
onMounted(async () => {
|
|
358
740
|
await consumeOauthBindingResult();
|
|
359
741
|
await loadOauthBindings();
|
|
742
|
+
await loadMfaStatus();
|
|
360
743
|
});
|
|
361
744
|
</script>
|
|
362
745
|
|
|
@@ -366,34 +749,25 @@ onMounted(async () => {
|
|
|
366
749
|
flex-direction: column;
|
|
367
750
|
gap: 20px;
|
|
368
751
|
}
|
|
369
|
-
.security-card
|
|
370
|
-
.status-card {
|
|
752
|
+
.security-card {
|
|
371
753
|
border: 1px solid #ebeef5;
|
|
372
|
-
border-radius:
|
|
754
|
+
border-radius: var(--admin-page-radius);
|
|
373
755
|
}
|
|
374
|
-
:deep(.security-card .el-card__body)
|
|
375
|
-
:deep(.status-card .el-card__body) {
|
|
756
|
+
:deep(.security-card .el-card__body) {
|
|
376
757
|
padding: 20px;
|
|
377
758
|
}
|
|
378
|
-
|
|
379
|
-
padding: 18px 20px;
|
|
380
|
-
border-bottom: 1px solid #f0f2f5;
|
|
381
|
-
}
|
|
382
|
-
.security-intro,
|
|
383
|
-
.status-header {
|
|
759
|
+
.security-intro {
|
|
384
760
|
display: flex;
|
|
385
761
|
gap: 16px;
|
|
386
762
|
align-items: flex-start;
|
|
387
763
|
justify-content: space-between;
|
|
388
764
|
}
|
|
389
|
-
.security-intro h3
|
|
390
|
-
.status-header h3 {
|
|
765
|
+
.security-intro h3 {
|
|
391
766
|
margin: 0;
|
|
392
767
|
font-size: 18px;
|
|
393
768
|
color: #303133;
|
|
394
769
|
}
|
|
395
|
-
.security-intro p
|
|
396
|
-
.status-header p {
|
|
770
|
+
.security-intro p {
|
|
397
771
|
margin: 6px 0 0;
|
|
398
772
|
font-size: 13px;
|
|
399
773
|
color: #909399;
|
|
@@ -404,8 +778,7 @@ onMounted(async () => {
|
|
|
404
778
|
gap: 12px;
|
|
405
779
|
margin-top: 16px;
|
|
406
780
|
}
|
|
407
|
-
.security-item
|
|
408
|
-
.status-item {
|
|
781
|
+
.security-item {
|
|
409
782
|
display: flex;
|
|
410
783
|
gap: 18px;
|
|
411
784
|
align-items: center;
|
|
@@ -413,7 +786,7 @@ onMounted(async () => {
|
|
|
413
786
|
padding: 16px;
|
|
414
787
|
background: #ffffff;
|
|
415
788
|
border: 1px solid #f0f2f5;
|
|
416
|
-
border-radius:
|
|
789
|
+
border-radius: var(--admin-page-radius);
|
|
417
790
|
}
|
|
418
791
|
.security-item__content {
|
|
419
792
|
min-width: 0;
|
|
@@ -440,40 +813,80 @@ onMounted(async () => {
|
|
|
440
813
|
object-fit: contain;
|
|
441
814
|
}
|
|
442
815
|
}
|
|
443
|
-
.security-item strong
|
|
444
|
-
.status-item strong {
|
|
816
|
+
.security-item strong {
|
|
445
817
|
display: block;
|
|
446
818
|
margin-bottom: 6px;
|
|
447
819
|
font-size: 15px;
|
|
448
820
|
color: #303133;
|
|
449
821
|
}
|
|
450
|
-
.security-item p
|
|
451
|
-
.status-item span {
|
|
822
|
+
.security-item p {
|
|
452
823
|
margin: 0;
|
|
453
824
|
font-size: 13px;
|
|
454
825
|
line-height: 1.6;
|
|
455
826
|
color: #909399;
|
|
456
827
|
}
|
|
457
|
-
.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
gap: 12px;
|
|
828
|
+
.security-item__hint {
|
|
829
|
+
margin-top: 4px !important;
|
|
830
|
+
color: var(--el-color-warning) !important;
|
|
461
831
|
}
|
|
462
832
|
.dialog-footer {
|
|
463
833
|
display: flex;
|
|
464
834
|
justify-content: flex-end;
|
|
465
835
|
}
|
|
836
|
+
.security-item__actions {
|
|
837
|
+
display: flex;
|
|
838
|
+
flex-wrap: wrap;
|
|
839
|
+
gap: 8px;
|
|
840
|
+
justify-content: flex-end;
|
|
841
|
+
}
|
|
842
|
+
.mfa-dialog-content {
|
|
843
|
+
min-width: 0;
|
|
844
|
+
}
|
|
845
|
+
.mfa-dialog-recovery {
|
|
846
|
+
margin-top: 12px;
|
|
847
|
+
}
|
|
848
|
+
.mfa-copyable-value {
|
|
849
|
+
display: flex;
|
|
850
|
+
gap: 8px;
|
|
851
|
+
align-items: flex-start;
|
|
852
|
+
line-height: 1.6;
|
|
853
|
+
color: var(--el-text-color-regular);
|
|
854
|
+
overflow-wrap: anywhere;
|
|
855
|
+
}
|
|
856
|
+
.mfa-copyable-value__text {
|
|
857
|
+
flex: 1;
|
|
858
|
+
min-width: 0;
|
|
859
|
+
white-space: pre-wrap;
|
|
860
|
+
}
|
|
861
|
+
.mfa-copy-button {
|
|
862
|
+
flex: 0 0 auto;
|
|
863
|
+
margin-top: -4px;
|
|
864
|
+
}
|
|
865
|
+
.mfa-copy-button__icon {
|
|
866
|
+
color: var(--el-color-primary);
|
|
867
|
+
font-size: 18px;
|
|
868
|
+
}
|
|
869
|
+
.mfa-recovery-hint {
|
|
870
|
+
line-height: 1.6;
|
|
871
|
+
color: var(--el-text-color-regular);
|
|
872
|
+
}
|
|
873
|
+
.mfa-recovery-hint {
|
|
874
|
+
margin: 8px 0 0;
|
|
875
|
+
font-size: 13px;
|
|
876
|
+
color: var(--el-color-warning);
|
|
877
|
+
}
|
|
878
|
+
.mfa-dialog-content :deep(.pro-form) {
|
|
879
|
+
margin: 0;
|
|
880
|
+
}
|
|
881
|
+
.mfa-dialog-content :deep(.mfa-dialog-form .el-form-item) {
|
|
882
|
+
margin-bottom: 16px;
|
|
883
|
+
}
|
|
466
884
|
|
|
467
885
|
@media screen and (width <= 768px) {
|
|
468
886
|
.security-intro,
|
|
469
|
-
.
|
|
470
|
-
.security-item,
|
|
471
|
-
.status-item {
|
|
887
|
+
.security-item {
|
|
472
888
|
flex-direction: column;
|
|
473
889
|
align-items: flex-start;
|
|
474
890
|
}
|
|
475
|
-
.status-grid {
|
|
476
|
-
grid-template-columns: 1fr;
|
|
477
|
-
}
|
|
478
891
|
}
|
|
479
892
|
</style>
|