@liujitcn/kratos-admin-core 0.0.31 → 0.0.33
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 +10 -4
- package/dist/package/declarations/src/api/base/{config.d.ts → v1/config.d.ts} +1 -1
- package/dist/package/declarations/src/api/base/{file.d.ts → v1/file.d.ts} +3 -1
- package/dist/package/declarations/src/api/base/{language.d.ts → v1/language.d.ts} +1 -1
- package/dist/package/declarations/src/api/base/{login.d.ts → v1/login.d.ts} +2 -2
- package/dist/package/declarations/src/api/base/{mfa.d.ts → v1/mfa.d.ts} +3 -3
- package/dist/package/declarations/src/api/base/{oauth.d.ts → v1/oauth.d.ts} +1 -1
- package/dist/package/declarations/src/api/system/{auth.d.ts → admin/v1/auth.d.ts} +2 -2
- package/dist/package/declarations/src/api/system/{base_dict.d.ts → admin/v1/base_dict.d.ts} +1 -1
- package/dist/package/declarations/src/api/system/{base_tenant.d.ts → admin/v1/base_tenant.d.ts} +2 -2
- package/dist/package/declarations/src/components/ProTable/index.vue.d.ts +114 -0
- package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +84 -76
- package/dist/package/declarations/src/components/Upload/File.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/WangEditor/index.vue.d.ts +2 -0
- package/dist/package/declarations/src/documentTitle.d.ts +2 -0
- package/dist/package/declarations/src/format.d.ts +1 -1
- package/dist/package/declarations/src/index.d.ts +2 -1
- package/dist/package/declarations/src/locales/generated.d.ts +68 -0
- package/dist/package/declarations/src/modules/index.d.ts +1 -3
- package/dist/package/declarations/src/request.d.ts +1 -1
- package/dist/package/declarations/src/routers/index.d.ts +1 -1
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +5 -3
- package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +13 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +0 -101
- package/dist/package/declarations/src/rpc/system/common/v1/common.d.ts +26 -0
- package/dist/package/declarations/src/stores/interface/index.d.ts +6 -1
- package/dist/package/declarations/src/stores/modules/user.d.ts +2 -4
- package/dist/package/declarations/src/utils/passwordStrength.d.ts +1 -12
- package/dist/package/declarations/src/utils/request.d.ts +13 -2
- package/dist/package/declarations/src/utils/utils.d.ts +4 -0
- package/dist/package/src/api/base/{config.ts → v1/config.ts} +2 -2
- package/dist/package/src/api/base/{file.ts → v1/file.ts} +20 -24
- package/dist/package/src/api/base/{language.ts → v1/language.ts} +2 -2
- package/dist/package/src/api/base/{login.ts → v1/login.ts} +3 -3
- package/dist/package/src/api/base/{mfa.ts → v1/mfa.ts} +4 -4
- package/dist/package/src/api/base/{oauth.ts → v1/oauth.ts} +2 -2
- package/dist/package/src/api/system/{auth.ts → admin/v1/auth.ts} +3 -3
- package/dist/package/src/api/system/{base_dict.ts → admin/v1/base_dict.ts} +2 -2
- package/dist/package/src/api/system/{base_tenant.ts → admin/v1/base_tenant.ts} +3 -3
- package/dist/package/src/bootstrap.ts +6 -5
- package/dist/package/src/components/CronExpression/index.vue +4 -3
- package/dist/package/src/components/Dialog/ProDialog.vue +1 -0
- package/dist/package/src/components/ImportExcel/index.vue +4 -2
- package/dist/package/src/components/MarkdownPreview/index.vue +1 -1
- package/dist/package/src/components/PasswordStrength/index.vue +1 -1
- package/dist/package/src/components/ProTable/components/TableColumn.vue +14 -6
- package/dist/package/src/components/ProTable/index.vue +3 -3
- package/dist/package/src/components/RichTextPreview/index.vue +12 -1
- package/dist/package/src/components/SelectIcon/index.vue +3 -2
- package/dist/package/src/components/Upload/File.vue +2 -2
- package/dist/package/src/components/Upload/Files.vue +2 -2
- package/dist/package/src/components/Upload/Img.vue +7 -5
- package/dist/package/src/components/Upload/Imgs.vue +6 -5
- package/dist/package/src/components/WangEditor/index.vue +10 -7
- package/dist/package/src/documentTitle.ts +8 -0
- package/dist/package/src/format.ts +1 -1
- package/dist/package/src/index.ts +2 -1
- package/dist/package/src/layouts/components/Header/components/Avatar.vue +7 -6
- package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +3 -2
- package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +3 -2
- package/dist/package/src/layouts/components/Header/components/SearchMenu.vue +16 -8
- package/dist/package/src/layouts/components/LockScreen.vue +175 -52
- package/dist/package/src/layouts/components/Tabs/index.vue +12 -3
- package/dist/package/src/layouts/components/ThemeDrawer/index.scss +4 -4
- package/dist/package/src/locales/en-US.json +15 -2
- package/dist/package/src/locales/ja-JP.json +15 -2
- package/dist/package/src/locales/zh-CN.json +15 -2
- package/dist/package/src/locales/zh-TW.json +15 -2
- package/dist/package/src/modules/index.ts +2 -4
- package/dist/package/src/request.ts +3 -0
- package/dist/package/src/routers/index.ts +15 -14
- package/dist/package/src/rpc/base/v1/config.ts +1 -1
- package/dist/package/src/rpc/base/v1/file.ts +1 -1
- package/dist/package/src/rpc/base/v1/language.ts +1 -1
- package/dist/package/src/rpc/base/v1/login.ts +5 -3
- package/dist/package/src/rpc/base/v1/mfa.ts +1 -1
- package/dist/package/src/rpc/base/v1/oauth.ts +1 -1
- package/dist/package/src/rpc/base/v1/oauth_client.ts +1 -1
- package/dist/package/src/rpc/common/v1/common.ts +1 -1
- package/dist/package/src/rpc/common/v1/enum.ts +1 -1
- package/dist/package/src/rpc/common/v1/types.ts +1 -1
- package/dist/package/src/rpc/google/protobuf/empty.ts +1 -1
- package/dist/package/src/rpc/google/protobuf/wrappers.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/auth.ts +14 -1
- package/dist/package/src/rpc/system/admin/v1/base_dict.ts +1 -113
- package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +1 -1
- package/dist/package/src/rpc/system/admin/v1/common.ts +1 -1
- package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
- package/dist/package/src/stores/interface/index.ts +6 -1
- package/dist/package/src/stores/modules/auth.ts +1 -1
- package/dist/package/src/stores/modules/config.ts +2 -2
- package/dist/package/src/stores/modules/dict.ts +1 -1
- package/dist/package/src/stores/modules/user.ts +21 -22
- package/dist/package/src/styles/element.scss +35 -4
- package/dist/package/src/typings/global.d.ts +6 -0
- package/dist/package/src/utils/passwordCrypto.ts +169 -12
- package/dist/package/src/utils/passwordStrength.ts +7 -24
- package/dist/package/src/utils/request.ts +155 -36
- package/dist/package/src/utils/tenant.ts +1 -1
- package/dist/package/src/utils/utils.ts +25 -6
- package/dist/package/src/views/login/components/LoginForm.vue +36 -21
- package/dist/package/src/views/login/index.scss +2 -2
- package/dist/package/types/generated/auto-imports.d.ts +2 -0
- package/package.json +2 -1
- package/types/generated/auto-imports.d.ts +2 -0
- package/vite.config.ts +24 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
2
|
+
// versions:
|
|
3
|
+
// protoc-gen-ts_proto v2.12.1
|
|
4
|
+
// protoc unknown
|
|
5
|
+
// source: system/common/v1/common.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
/** 系统公共用户相关枚举。 */
|
|
10
|
+
export enum BaseUserIDType {
|
|
11
|
+
/** BASE_USER_ID_TYPE_UNSPECIFIED - 未指定证件类型。 */
|
|
12
|
+
BASE_USER_ID_TYPE_UNSPECIFIED = 0,
|
|
13
|
+
/** BASE_USER_ID_TYPE_ID_CARD - 居民身份证。 */
|
|
14
|
+
BASE_USER_ID_TYPE_ID_CARD = 1,
|
|
15
|
+
/** BASE_USER_ID_TYPE_PASSPORT - 护照。 */
|
|
16
|
+
BASE_USER_ID_TYPE_PASSPORT = 2,
|
|
17
|
+
/** BASE_USER_ID_TYPE_HK_MACAO_PERMIT - 港澳居民来往内地通行证。 */
|
|
18
|
+
BASE_USER_ID_TYPE_HK_MACAO_PERMIT = 3,
|
|
19
|
+
/** BASE_USER_ID_TYPE_TAIWAN_PERMIT - 台湾居民来往大陆通行证。 */
|
|
20
|
+
BASE_USER_ID_TYPE_TAIWAN_PERMIT = 4,
|
|
21
|
+
/** BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE - 外国人永久居留身份证。 */
|
|
22
|
+
BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE = 5,
|
|
23
|
+
/** BASE_USER_ID_TYPE_OTHER - 其他证件。 */
|
|
24
|
+
BASE_USER_ID_TYPE_OTHER = 6,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 用户密码修改状态。 */
|
|
28
|
+
export enum BaseUserPasswordChangeStatus {
|
|
29
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED - 未指定密码修改状态。 */
|
|
30
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED = 0,
|
|
31
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED - 当前必须修改密码。 */
|
|
32
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED = 1,
|
|
33
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED - 当前无需修改密码。 */
|
|
34
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED = 2,
|
|
35
|
+
}
|
|
@@ -53,9 +53,14 @@ export interface GlobalState {
|
|
|
53
53
|
/** 当前登录用户认证状态。 */
|
|
54
54
|
export interface UserState {
|
|
55
55
|
token: string;
|
|
56
|
-
refreshToken: string;
|
|
57
56
|
tokenType: string;
|
|
58
57
|
tokenExpiresAt: number;
|
|
58
|
+
/** 是否正在执行退出登录,阻止后台请求刷新令牌。 */
|
|
59
|
+
isLoggingOut: boolean;
|
|
60
|
+
/** 认证状态版本,用于丢弃退出期间完成的旧刷新请求。 */
|
|
61
|
+
authVersion: number;
|
|
62
|
+
/** 当前认证是否已被本地显式清理。 */
|
|
63
|
+
authInvalidated: boolean;
|
|
59
64
|
userInfo: UserInfoForm;
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineStore } from "pinia";
|
|
2
|
-
import { defAuthService } from "../../api/system/auth";
|
|
2
|
+
import { defAuthService } from "../../api/system/admin/v1/auth";
|
|
3
3
|
import { AuthState } from "../../stores/interface";
|
|
4
4
|
import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList, isExternalPath } from "../../utils";
|
|
5
5
|
import type { RouteItem } from "../../rpc/system/admin/v1/auth";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineStore } from "pinia";
|
|
2
|
-
import { defConfigService } from "../../api/base/config";
|
|
2
|
+
import { defConfigService } from "../../api/base/v1/config";
|
|
3
3
|
import { BaseConfigSite } from "../../rpc/base/v1/config";
|
|
4
4
|
import type { LoginCaptchaConfig, SiteConfigState, SiteDisplayConfig } from "../../stores/interface";
|
|
5
5
|
import defaultLogoUrl from "../../assets/images/logo.svg";
|
|
@@ -9,7 +9,7 @@ const CAPTCHA_TYPE_KEY = "captchaType";
|
|
|
9
9
|
const I18N_DRAFT_ENABLED_KEY = "i18n.draft.enabled";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_SITE_DISPLAY_CONFIG: SiteDisplayConfig = {
|
|
12
|
-
sysName:
|
|
12
|
+
sysName: import.meta.env.VITE_GLOB_APP_TITLE,
|
|
13
13
|
icp: "",
|
|
14
14
|
copyright: "2026 © Admin",
|
|
15
15
|
watermark: "Admin",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineStore } from "pinia";
|
|
2
2
|
import pinia from "../../stores";
|
|
3
|
-
import { defBaseDictService } from "../../api/system/base_dict";
|
|
3
|
+
import { defBaseDictService } from "../../api/system/admin/v1/base_dict";
|
|
4
4
|
import type { DictState } from "../../stores/interface";
|
|
5
5
|
import piniaPersistConfig from "../../stores/helper/persist";
|
|
6
6
|
import type { OptionBaseDictResponse_BaseDict, OptionBaseDictResponse_BaseDictItem } from "../../rpc/system/admin/v1/base_dict";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineStore } from "pinia";
|
|
2
|
-
import { defAuthService } from "../../api/system/auth";
|
|
3
|
-
import { defLoginService } from "../../api/base/login";
|
|
4
|
-
import { defMfaService } from "../../api/base/mfa";
|
|
2
|
+
import { defAuthService } from "../../api/system/admin/v1/auth";
|
|
3
|
+
import { defLoginService } from "../../api/base/v1/login";
|
|
4
|
+
import { defMfaService } from "../../api/base/v1/mfa";
|
|
5
5
|
import type { LoginRequest, LoginResponse } from "../../rpc/base/v1/login";
|
|
6
6
|
import type { VerifyMfaRequest } from "../../rpc/base/v1/mfa";
|
|
7
7
|
import type { UserInfoForm } from "../../rpc/system/admin/v1/auth";
|
|
@@ -9,12 +9,14 @@ import { UserState } from "../../stores/interface";
|
|
|
9
9
|
import piniaPersistConfig from "../../stores/helper/persist";
|
|
10
10
|
import { useDictStoreHook } from "../../stores/modules/dict";
|
|
11
11
|
import { useLockScreenStore } from "../../stores/modules/lockScreen";
|
|
12
|
-
import { t } from "../../locales";
|
|
13
12
|
|
|
14
13
|
const defaultUserInfo: UserInfoForm = {
|
|
15
14
|
user_name: "",
|
|
16
15
|
nick_name: "",
|
|
17
16
|
phone: "",
|
|
17
|
+
email: "",
|
|
18
|
+
id_type: 0,
|
|
19
|
+
id_code: "",
|
|
18
20
|
avatar: "",
|
|
19
21
|
role_code: "",
|
|
20
22
|
role_name: "",
|
|
@@ -26,9 +28,11 @@ const defaultUserInfo: UserInfoForm = {
|
|
|
26
28
|
export const useUserStore = defineStore("admin-user", {
|
|
27
29
|
state: (): UserState => ({
|
|
28
30
|
token: "",
|
|
29
|
-
refreshToken: "",
|
|
30
31
|
tokenType: "",
|
|
31
32
|
tokenExpiresAt: 0,
|
|
33
|
+
isLoggingOut: false,
|
|
34
|
+
authVersion: 0,
|
|
35
|
+
authInvalidated: false,
|
|
32
36
|
userInfo: defaultUserInfo
|
|
33
37
|
}),
|
|
34
38
|
getters: {},
|
|
@@ -37,10 +41,6 @@ export const useUserStore = defineStore("admin-user", {
|
|
|
37
41
|
setToken(token: string) {
|
|
38
42
|
this.token = token;
|
|
39
43
|
},
|
|
40
|
-
/** 设置刷新令牌 */
|
|
41
|
-
setRefreshToken(refreshToken: string) {
|
|
42
|
-
this.refreshToken = refreshToken;
|
|
43
|
-
},
|
|
44
44
|
/** 设置令牌类型 */
|
|
45
45
|
setTokenType(tokenType: string) {
|
|
46
46
|
this.tokenType = tokenType;
|
|
@@ -54,12 +54,12 @@ export const useUserStore = defineStore("admin-user", {
|
|
|
54
54
|
this.userInfo = userInfo;
|
|
55
55
|
},
|
|
56
56
|
/** 根据接口返回统一更新令牌信息 */
|
|
57
|
-
updateTokenAuth(accessToken: string,
|
|
57
|
+
updateTokenAuth(accessToken: string, tokenType: string, expiresIn?: number) {
|
|
58
58
|
const tokenPrefix = tokenType ? `${tokenType} ` : "";
|
|
59
59
|
const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 : 0;
|
|
60
60
|
|
|
61
|
+
this.authInvalidated = false;
|
|
61
62
|
this.setToken(`${tokenPrefix}${accessToken}`.trim());
|
|
62
|
-
this.setRefreshToken(refreshToken ?? "");
|
|
63
63
|
this.setTokenType(tokenType ?? "");
|
|
64
64
|
this.setTokenExpiresAt(expiresAt);
|
|
65
65
|
},
|
|
@@ -71,24 +71,20 @@ export const useUserStore = defineStore("admin-user", {
|
|
|
71
71
|
async login(loginRequest: LoginRequest): Promise<LoginResponse> {
|
|
72
72
|
const data = await defLoginService.Login(loginRequest);
|
|
73
73
|
if (data.status === 1 || data.status === 0 || data.status === 4) {
|
|
74
|
-
this.updateTokenAuth(data.access_token, data.
|
|
74
|
+
this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
|
|
75
75
|
}
|
|
76
76
|
return data;
|
|
77
77
|
},
|
|
78
78
|
/** 校验登录阶段的多因素认证并保存正式令牌。 */
|
|
79
79
|
async verifyMfa(request: VerifyMfaRequest): Promise<LoginResponse> {
|
|
80
80
|
const data = await defMfaService.VerifyMfa(request);
|
|
81
|
-
this.updateTokenAuth(data.access_token, data.
|
|
81
|
+
this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
|
|
82
82
|
return data;
|
|
83
83
|
},
|
|
84
|
-
/**
|
|
84
|
+
/** 通过 HttpOnly Cookie 刷新认证令牌。 */
|
|
85
85
|
async refreshAccessToken() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const data = await defLoginService.RefreshToken({ refresh_token: this.refreshToken });
|
|
91
|
-
this.updateTokenAuth(data.access_token, data.refresh_token ?? this.refreshToken, data.token_type ?? "", data.expires_in);
|
|
86
|
+
const data = await defLoginService.RefreshToken({});
|
|
87
|
+
this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
|
|
92
88
|
return data;
|
|
93
89
|
},
|
|
94
90
|
/** 获取用户信息 */
|
|
@@ -102,22 +98,25 @@ export const useUserStore = defineStore("admin-user", {
|
|
|
102
98
|
// 清理登录态时同步清空字典缓存,避免切换账号后读到旧字典。
|
|
103
99
|
useDictStoreHook().clearDictionaryCache();
|
|
104
100
|
useLockScreenStore().clearLock();
|
|
101
|
+
this.authVersion += 1;
|
|
102
|
+
this.authInvalidated = true;
|
|
105
103
|
this.setToken("");
|
|
106
|
-
this.setRefreshToken("");
|
|
107
104
|
this.setTokenType("");
|
|
108
105
|
this.setTokenExpiresAt(0);
|
|
109
106
|
this.setUserInfo({ ...defaultUserInfo });
|
|
110
107
|
},
|
|
111
108
|
/** 退出登录 */
|
|
112
109
|
async logout() {
|
|
110
|
+
this.isLoggingOut = true;
|
|
113
111
|
try {
|
|
114
112
|
await defLoginService.Logout({});
|
|
115
113
|
} catch {
|
|
116
114
|
// 退出接口返回异常时,前端仍需清理本地登录态,避免用户卡在当前会话。
|
|
117
115
|
} finally {
|
|
118
116
|
this.clearAuthData();
|
|
117
|
+
this.isLoggingOut = false;
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
120
|
},
|
|
122
|
-
persist: piniaPersistConfig("admin-user", ["
|
|
121
|
+
persist: piniaPersistConfig("admin-user-security-v2", ["userInfo"])
|
|
123
122
|
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/* 统一 Element Plus 矩形控件与后台页面的圆角。 */
|
|
2
|
+
html:root {
|
|
3
|
+
--el-border-radius-base: var(--admin-page-radius);
|
|
4
|
+
--el-border-radius-small: 2px;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
/* el-alert */
|
|
2
8
|
.el-alert {
|
|
3
9
|
border: 1px solid;
|
|
@@ -27,7 +33,18 @@
|
|
|
27
33
|
overflow-x: hidden;
|
|
28
34
|
background-color: var(--el-bg-color);
|
|
29
35
|
border: 1px solid var(--el-border-color-light);
|
|
30
|
-
border-radius:
|
|
36
|
+
border-radius: var(--admin-page-radius);
|
|
37
|
+
box-shadow: 0 0 12px rgb(0 0 0 / 5%);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 非列表业务页沿用 ProTable 卡片外观 */
|
|
41
|
+
.el-card.admin-page-card {
|
|
42
|
+
background-color: var(--el-bg-color);
|
|
43
|
+
border: 1px solid var(--el-border-color-light);
|
|
44
|
+
border-radius: var(--admin-page-radius);
|
|
45
|
+
box-shadow: 0 0 12px rgb(0 0 0 / 5%);
|
|
46
|
+
}
|
|
47
|
+
.el-card.admin-page-card.is-always-shadow {
|
|
31
48
|
box-shadow: 0 0 12px rgb(0 0 0 / 5%);
|
|
32
49
|
}
|
|
33
50
|
|
|
@@ -165,10 +182,9 @@
|
|
|
165
182
|
}
|
|
166
183
|
}
|
|
167
184
|
|
|
168
|
-
// 设置 el-table
|
|
185
|
+
// 设置 el-table 表头文字单行省略,完整内容由列标题悬浮提示展示。
|
|
169
186
|
.el-table__header .el-table__cell > .cell {
|
|
170
|
-
|
|
171
|
-
white-space: wrap;
|
|
187
|
+
white-space: nowrap;
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
// 空表时去掉底部那条默认分隔线,和菜单管理页保持一致。
|
|
@@ -328,6 +344,21 @@
|
|
|
328
344
|
}
|
|
329
345
|
}
|
|
330
346
|
|
|
347
|
+
// ProDialog 需要去掉 Element Plus 默认的外层内边距,避免异步样式重复加载后覆盖紧凑布局。
|
|
348
|
+
.el-dialog.pro-dialog {
|
|
349
|
+
padding: 0;
|
|
350
|
+
|
|
351
|
+
// 对齐原生 MessageBox 的紧凑上下间距,避免标题和操作区留白过大。
|
|
352
|
+
.el-dialog__header,
|
|
353
|
+
.el-dialog__body {
|
|
354
|
+
padding: 12px 20px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.el-dialog__footer {
|
|
358
|
+
padding: 0 20px 12px;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
331
362
|
@media screen and (width <= 768px) {
|
|
332
363
|
.el-dialog {
|
|
333
364
|
margin-top: 4vh;
|
|
@@ -55,6 +55,12 @@ declare interface ViteEnv {
|
|
|
55
55
|
VITE_PUBLIC_PATH: string;
|
|
56
56
|
VITE_API_URL: string;
|
|
57
57
|
VITE_PROXY: [string, string][];
|
|
58
|
+
/** 是否启用 Vite HTTPS 开发服务。 */
|
|
59
|
+
VITE_HTTPS: boolean;
|
|
60
|
+
/** HTTPS 私钥路径,相对于宿主 apps/admin 目录。 */
|
|
61
|
+
VITE_HTTPS_KEY: string;
|
|
62
|
+
/** HTTPS 证书路径,相对于宿主 apps/admin 目录。 */
|
|
63
|
+
VITE_HTTPS_CERT: string;
|
|
58
64
|
VITE_CODEINSPECTOR: boolean;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { defLoginService } from "../api/base/login";
|
|
2
|
-
import { PasswordCryptoScene } from "../rpc/base/v1/login";
|
|
1
|
+
import { defLoginService } from "../api/base/v1/login";
|
|
2
|
+
import { PasswordCryptoScene, type PasswordPublicKeyResponse } from "../rpc/base/v1/login";
|
|
3
3
|
import type { PasswordCrypto } from "../rpc/common/v1/types";
|
|
4
4
|
import { t } from "../locales";
|
|
5
|
+
import * as portableCrypto from "asmcrypto.js";
|
|
6
|
+
|
|
7
|
+
type PortableCrypto = typeof import("asmcrypto.js");
|
|
5
8
|
|
|
6
9
|
export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene;
|
|
7
10
|
export type { PasswordCryptoScene };
|
|
@@ -12,7 +15,11 @@ function pemToArrayBuffer(pem: string) {
|
|
|
12
15
|
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
|
|
13
16
|
.replace(/-----END PUBLIC KEY-----/g, "")
|
|
14
17
|
.replace(/\s/g, "");
|
|
15
|
-
const
|
|
18
|
+
const decode = globalThis.atob;
|
|
19
|
+
if (!decode) {
|
|
20
|
+
throw new Error(t("core.password.base64_decode_unsupported"));
|
|
21
|
+
}
|
|
22
|
+
const binary = decode(base64);
|
|
16
23
|
const buffer = new Uint8Array(binary.length);
|
|
17
24
|
for (let i = 0; i < binary.length; i += 1) {
|
|
18
25
|
buffer[i] = binary.charCodeAt(i);
|
|
@@ -27,18 +34,163 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
|
27
34
|
for (let i = 0; i < bytes.byteLength; i += 1) {
|
|
28
35
|
binary += String.fromCharCode(bytes[i]);
|
|
29
36
|
}
|
|
30
|
-
|
|
37
|
+
const encode = globalThis.btoa;
|
|
38
|
+
if (!encode) {
|
|
39
|
+
throw new Error(t("core.password.base64_encode_unsupported"));
|
|
40
|
+
}
|
|
41
|
+
return encode(binary);
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
/** 获取浏览器 WebCrypto 能力。 */
|
|
34
45
|
function getSubtleCrypto() {
|
|
35
46
|
const cryptoApi = globalThis.crypto;
|
|
36
|
-
|
|
47
|
+
const isSecureContext = typeof window === "undefined" || window.isSecureContext;
|
|
48
|
+
if (!cryptoApi?.subtle || !isSecureContext) {
|
|
37
49
|
throw new Error(t("core.password.crypto_unsupported"));
|
|
38
50
|
}
|
|
39
51
|
return cryptoApi;
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
|
|
55
|
+
function readDerNode(data: Uint8Array, offset: number) {
|
|
56
|
+
if (offset + 2 > data.length) {
|
|
57
|
+
throw new Error(t("core.password.public_key_invalid"));
|
|
58
|
+
}
|
|
59
|
+
const tag = data[offset];
|
|
60
|
+
const firstLengthByte = data[offset + 1];
|
|
61
|
+
let length = firstLengthByte;
|
|
62
|
+
let valueOffset = offset + 2;
|
|
63
|
+
if (firstLengthByte & 0x80) {
|
|
64
|
+
const lengthByteCount = firstLengthByte & 0x7f;
|
|
65
|
+
if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
|
|
66
|
+
throw new Error(t("core.password.public_key_invalid"));
|
|
67
|
+
}
|
|
68
|
+
length = 0;
|
|
69
|
+
for (let index = 0; index < lengthByteCount; index += 1) {
|
|
70
|
+
length = length * 256 + data[valueOffset + index];
|
|
71
|
+
}
|
|
72
|
+
valueOffset += lengthByteCount;
|
|
73
|
+
}
|
|
74
|
+
const end = valueOffset + length;
|
|
75
|
+
if (end > data.length) {
|
|
76
|
+
throw new Error(t("core.password.public_key_invalid"));
|
|
77
|
+
}
|
|
78
|
+
return { tag, value: data.subarray(valueOffset, end), next: end };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 去除 DER 正整数前导零字节。 */
|
|
82
|
+
function trimDerInteger(value: Uint8Array) {
|
|
83
|
+
let offset = 0;
|
|
84
|
+
while (offset < value.length - 1 && value[offset] === 0) {
|
|
85
|
+
offset += 1;
|
|
86
|
+
}
|
|
87
|
+
return value.subarray(offset);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 从 PEM SubjectPublicKeyInfo 中提取 RSA 模数和指数。 */
|
|
91
|
+
function parsePortableRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
|
|
92
|
+
const der = new Uint8Array(pemToArrayBuffer(publicKey));
|
|
93
|
+
const subjectPublicKeyInfo = readDerNode(der, 0);
|
|
94
|
+
const algorithm = readDerNode(subjectPublicKeyInfo.value, 0);
|
|
95
|
+
const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next);
|
|
96
|
+
if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
|
|
97
|
+
throw new Error(t("core.password.public_key_invalid"));
|
|
98
|
+
}
|
|
99
|
+
const rsaPublicKey = readDerNode(bitString.value, 1);
|
|
100
|
+
const modulus = readDerNode(rsaPublicKey.value, 0);
|
|
101
|
+
const exponent = readDerNode(rsaPublicKey.value, modulus.next);
|
|
102
|
+
if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
|
|
103
|
+
throw new Error(t("core.password.public_key_invalid"));
|
|
104
|
+
}
|
|
105
|
+
return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 计算 RSA-OAEP 所需的 MGF1 掩码。 */
|
|
109
|
+
function generateMgf1Mask(cryptoApi: PortableCrypto, seed: Uint8Array, length: number) {
|
|
110
|
+
const mask = new Uint8Array(length);
|
|
111
|
+
const counter = new Uint8Array(4);
|
|
112
|
+
const hash = new cryptoApi.Sha256();
|
|
113
|
+
const blockCount = Math.ceil(length / hash.HASH_SIZE);
|
|
114
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
115
|
+
counter[0] = index >>> 24;
|
|
116
|
+
counter[1] = index >>> 16;
|
|
117
|
+
counter[2] = index >>> 8;
|
|
118
|
+
counter[3] = index;
|
|
119
|
+
let block = hash.reset().process(seed).process(counter).finish().result;
|
|
120
|
+
if (!block) {
|
|
121
|
+
throw new Error(t("core.password.encrypt_failed"));
|
|
122
|
+
}
|
|
123
|
+
const target = mask.subarray(index * hash.HASH_SIZE);
|
|
124
|
+
if (block.length > target.length) {
|
|
125
|
+
block = block.subarray(0, target.length);
|
|
126
|
+
}
|
|
127
|
+
target.set(block);
|
|
128
|
+
}
|
|
129
|
+
return mask;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 生成纯 JavaScript 密码加密所需的随机字节。 */
|
|
133
|
+
function getPortableRandomBytes(length: number) {
|
|
134
|
+
const cryptoApi = globalThis.crypto;
|
|
135
|
+
if (!cryptoApi?.getRandomValues) {
|
|
136
|
+
throw new Error(t("core.password.random_failed"));
|
|
137
|
+
}
|
|
138
|
+
return cryptoApi.getRandomValues(new Uint8Array(length));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的密码加密协议。 */
|
|
142
|
+
async function encryptPortablePassword(
|
|
143
|
+
cryptoApi: PortableCrypto,
|
|
144
|
+
password: string,
|
|
145
|
+
publicKeyResponse: PasswordPublicKeyResponse
|
|
146
|
+
): Promise<PasswordCrypto> {
|
|
147
|
+
const aesKey = getPortableRandomBytes(32);
|
|
148
|
+
const iv = getPortableRandomBytes(12);
|
|
149
|
+
const plaintext = cryptoApi.string_to_bytes(password, true);
|
|
150
|
+
const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key);
|
|
151
|
+
const hash = new cryptoApi.Sha256();
|
|
152
|
+
const keySize = Math.ceil(new cryptoApi.BigNumber(modulus).bitLength / 8);
|
|
153
|
+
const seed = getPortableRandomBytes(hash.HASH_SIZE);
|
|
154
|
+
const dataBlockLength = keySize - hash.HASH_SIZE - 1;
|
|
155
|
+
const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1;
|
|
156
|
+
if (paddingLength < 0) {
|
|
157
|
+
throw new Error(t("core.password.password_too_long"));
|
|
158
|
+
}
|
|
159
|
+
const encodedMessage = new Uint8Array(keySize);
|
|
160
|
+
const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1);
|
|
161
|
+
const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1);
|
|
162
|
+
const labelHash = hash.reset().process(new Uint8Array()).finish().result;
|
|
163
|
+
if (!labelHash) {
|
|
164
|
+
throw new Error(t("core.password.encrypt_failed"));
|
|
165
|
+
}
|
|
166
|
+
dataBlock.set(labelHash, 0);
|
|
167
|
+
dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1);
|
|
168
|
+
dataBlock[hash.HASH_SIZE + paddingLength] = 1;
|
|
169
|
+
const dataBlockMask = generateMgf1Mask(cryptoApi, seed, dataBlock.length);
|
|
170
|
+
for (let index = 0; index < dataBlock.length; index += 1) {
|
|
171
|
+
dataBlock[index] ^= dataBlockMask[index];
|
|
172
|
+
}
|
|
173
|
+
const seedMask = generateMgf1Mask(cryptoApi, dataBlock, maskedSeed.length);
|
|
174
|
+
maskedSeed.set(seed);
|
|
175
|
+
for (let index = 0; index < maskedSeed.length; index += 1) {
|
|
176
|
+
maskedSeed[index] ^= seedMask[index];
|
|
177
|
+
}
|
|
178
|
+
const encryptedKey = new cryptoApi.RSA([modulus, exponent]).encrypt(new cryptoApi.BigNumber(encodedMessage)).result;
|
|
179
|
+
if (!encryptedKey) {
|
|
180
|
+
throw new Error(t("core.password.encrypt_failed"));
|
|
181
|
+
}
|
|
182
|
+
const ciphertext = cryptoApi.AES_GCM.encrypt(plaintext, aesKey, iv);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
key_id: publicKeyResponse.key_id,
|
|
186
|
+
nonce: publicKeyResponse.nonce,
|
|
187
|
+
algorithm: publicKeyResponse.algorithm,
|
|
188
|
+
encrypted_key: arrayBufferToBase64(encryptedKey.slice().buffer),
|
|
189
|
+
iv: arrayBufferToBase64(iv.buffer),
|
|
190
|
+
ciphertext: arrayBufferToBase64(ciphertext.slice().buffer)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
42
194
|
/** 加密单个密码字段,返回后端可解析的密码密文。 */
|
|
43
195
|
export async function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto> {
|
|
44
196
|
const plainPassword = password.trim();
|
|
@@ -46,20 +198,25 @@ export async function encryptPassword(password: string, scene: PasswordCryptoSce
|
|
|
46
198
|
throw new Error(t("core.password.required"));
|
|
47
199
|
}
|
|
48
200
|
|
|
49
|
-
const cryptoApi = getSubtleCrypto();
|
|
50
201
|
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene });
|
|
51
|
-
const
|
|
202
|
+
const cryptoApi = globalThis.crypto;
|
|
203
|
+
const isSecureContext = typeof window === "undefined" || window.isSecureContext;
|
|
204
|
+
if (!cryptoApi?.subtle || !isSecureContext) {
|
|
205
|
+
return encryptPortablePassword(portableCrypto, plainPassword, publicKeyResponse);
|
|
206
|
+
}
|
|
207
|
+
const subtleCrypto = getSubtleCrypto();
|
|
208
|
+
const publicKey = await subtleCrypto.subtle.importKey(
|
|
52
209
|
"spki",
|
|
53
210
|
pemToArrayBuffer(publicKeyResponse.public_key),
|
|
54
211
|
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
55
212
|
false,
|
|
56
213
|
["encrypt"]
|
|
57
214
|
);
|
|
58
|
-
const aesKey = await
|
|
59
|
-
const rawAesKey = await
|
|
60
|
-
const iv =
|
|
61
|
-
const ciphertext = await
|
|
62
|
-
const encryptedKey = await
|
|
215
|
+
const aesKey = await subtleCrypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
|
|
216
|
+
const rawAesKey = await subtleCrypto.subtle.exportKey("raw", aesKey);
|
|
217
|
+
const iv = subtleCrypto.getRandomValues(new Uint8Array(12));
|
|
218
|
+
const ciphertext = await subtleCrypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(plainPassword));
|
|
219
|
+
const encryptedKey = await subtleCrypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKey, rawAesKey);
|
|
63
220
|
|
|
64
221
|
return {
|
|
65
222
|
key_id: publicKeyResponse.key_id,
|
|
@@ -9,12 +9,10 @@ export interface PasswordStrengthResult {
|
|
|
9
9
|
strengthScore: number;
|
|
10
10
|
/** 强度等级。 */
|
|
11
11
|
level: PasswordStrengthLevel;
|
|
12
|
-
/** 是否达到允许提交的最高强度。 */
|
|
13
|
-
isValid: boolean;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* 计算密码强度结果,供表单展示使用;最终规则由服务端密码策略校验。
|
|
18
16
|
*
|
|
19
17
|
* @param password 当前密码
|
|
20
18
|
* @returns 密码强度分析结果
|
|
@@ -24,14 +22,13 @@ export function getPasswordStrength(password?: string): PasswordStrengthResult {
|
|
|
24
22
|
return {
|
|
25
23
|
ruleScore: 0,
|
|
26
24
|
strengthScore: 0,
|
|
27
|
-
level: "empty"
|
|
28
|
-
isValid: false
|
|
25
|
+
level: "empty"
|
|
29
26
|
};
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
let ruleScore = 0;
|
|
33
|
-
if (password
|
|
34
|
-
if (/[
|
|
30
|
+
if (/[a-z]/.test(password)) ruleScore += 1;
|
|
31
|
+
if (/[A-Z]/.test(password)) ruleScore += 1;
|
|
35
32
|
if (/\d/.test(password)) ruleScore += 1;
|
|
36
33
|
if (/[^A-Za-z0-9]/.test(password)) ruleScore += 1;
|
|
37
34
|
|
|
@@ -39,33 +36,19 @@ export function getPasswordStrength(password?: string): PasswordStrengthResult {
|
|
|
39
36
|
return {
|
|
40
37
|
ruleScore,
|
|
41
38
|
strengthScore: 3,
|
|
42
|
-
level: "high"
|
|
43
|
-
isValid: true
|
|
39
|
+
level: "high"
|
|
44
40
|
};
|
|
45
41
|
}
|
|
46
42
|
if (ruleScore === 3) {
|
|
47
43
|
return {
|
|
48
44
|
ruleScore,
|
|
49
45
|
strengthScore: 2,
|
|
50
|
-
level: "medium"
|
|
51
|
-
isValid: false
|
|
46
|
+
level: "medium"
|
|
52
47
|
};
|
|
53
48
|
}
|
|
54
49
|
return {
|
|
55
50
|
ruleScore,
|
|
56
51
|
strengthScore: 1,
|
|
57
|
-
level: "low"
|
|
58
|
-
isValid: false
|
|
52
|
+
level: "low"
|
|
59
53
|
};
|
|
60
54
|
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 校验密码是否达到最高强度,便于表单规则直接复用。
|
|
64
|
-
*
|
|
65
|
-
* @param password 当前密码
|
|
66
|
-
* @returns 校验结果
|
|
67
|
-
*/
|
|
68
|
-
export function validatePasswordStrengthValue(password?: string) {
|
|
69
|
-
const result = getPasswordStrength(password);
|
|
70
|
-
return { valid: result.isValid };
|
|
71
|
-
}
|