Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"react-monaco-editor": "^0.43.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.3.0",
"sm-crypto": "0.3.13",
"sql-formatter": "^12.2.2",
"typescript": "^5.0.2",
"use-resize-observer": "^9.0.2",
Expand Down
26 changes: 26 additions & 0 deletions src/api/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2387,6 +2387,32 @@ export interface IUserLoginReqV1 {
password?: string;

username?: string;

encrypted_username?: string;

encrypted_password?: string;

key_id?: string;
}

export interface ILoginEncryptionResDataV1 {
enable?: boolean;

algorithm?: string;

cipher_mode?: string;

public_key?: string;

key_id?: string;
}

export interface IGetLoginEncryptionResV1 {
code?: number;

message?: string;

data?: ILoginEncryptionResDataV1;
}

export interface IUserLoginResV1 {
Expand Down
5 changes: 4 additions & 1 deletion src/api/user/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import {
IGetUsersResV1,
ICreateUserReqV1,
IUpdateUserReqV1,
IUpdateOtherUserPasswordReqV1
IUpdateOtherUserPasswordReqV1,
IGetLoginEncryptionResV1
} from '../common.d';

export interface ILoginV1Params extends IUserLoginReqV1 {}

export interface ILoginV1Return extends IGetUserLoginResV1 {}

export interface IGetLoginEncryptionV1Return extends IGetLoginEncryptionResV1 {}

export interface ILogoutV1Return extends IBaseRes {}

export interface IGetMemberTipListV1Params {
Expand Down
11 changes: 10 additions & 1 deletion src/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,19 @@ import {
IUpdateOtherUserPasswordV1Params,
IUpdateOtherUserPasswordV1Return,
ILoginV2Params,
ILoginV2Return
ILoginV2Return,
IGetLoginEncryptionV1Return
} from './index.d';

class UserService extends ServiceBase {
public getLoginEncryptionV1(options?: AxiosRequestConfig) {
return this.get<IGetLoginEncryptionV1Return>(
'/v1/login/encryption',
undefined,
options
);
}

public loginV1(params: ILoginV1Params, options?: AxiosRequestConfig) {
const paramsData = this.cloneDeep(params);
return this.post<ILoginV1Return>('/v1/login', paramsData, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ exports[`test Nav/Header/Modal should match snapshot 1`] = `
class="ant-typography"
>
UI Version:
feature/issue-110 72a0e52
feat/yumchina-login-sm2-encryption a855454
</h5>
</div>
<div
Expand Down
22 changes: 22 additions & 0 deletions src/locale/en-US/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,26 @@ export default {
powered: 'Action SQLe',
pageTitle: 'SQL Audit Platform',
login: 'Login',
otherMethod: 'Other login methods',

userAgreementTips: 'I have read and agree to the',
userAgreement: 'User Agreement',

errorMessage: {
userAgreement: 'Please read and agree to the user agreement first',
encryption: 'Login failed, please try again later',
},

oauth: {
title: 'User Binding',
form: {
username: 'Bound SQLE username',
},
submitButton: 'Bind and login',
bindTips: 'If the username does not exist, it will be created automatically',
errorTitle: 'OAuth login error',
lostToken: 'Token not found, please return to the login page and retry',
lostOauth2Token:
'OAuth token not found, please return to the login page and retry',
},
};
1 change: 1 addition & 0 deletions src/locale/zh-CN/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default {

errorMessage: {
userAgreement: '请先阅读并同意用户协议',
encryption: '登录失败,请稍后重试',
},

oauth: {
Expand Down
13 changes: 12 additions & 1 deletion src/page/Login/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {

import { fireEvent, screen, act } from '@testing-library/react';
import user from '../../api/user';
import { resolveThreeSecond } from '../../testUtils/mockRequest';
import {
resolveImmediately,
resolveThreeSecond,
} from '../../testUtils/mockRequest';
import configuration from '../../api/configuration';
import { useLocation } from 'react-router-dom';
import {
Expand Down Expand Up @@ -47,6 +50,7 @@ describe('Login', () => {
system: { webTitle: SQLE_DEFAULT_WEB_TITLE, webLogoUrl: '' },
})
);
mockGetLoginEncryption();
mockGetOauth2Tips();
jest.useFakeTimers();
useLocationMock.mockReturnValue({
Expand Down Expand Up @@ -86,6 +90,13 @@ describe('Login', () => {
return spy;
};

// plaintext path: keep existing loginV2 assertions; encryption API is a mount dep
const mockGetLoginEncryption = () => {
const spy = jest.spyOn(user, 'getLoginEncryptionV1');
spy.mockImplementation(() => resolveImmediately({ enable: false }));
return spy;
};

const mockGetOauth2Tips = () => {
const spy = jest.spyOn(configuration, 'getOauth2Tips');
spy.mockImplementation(() =>
Expand Down
108 changes: 86 additions & 22 deletions src/page/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';

import './index.less';
import { Button, Checkbox, Form, Input, message, Typography } from 'antd';
Expand All @@ -21,14 +21,69 @@
import { getCookie } from '../../utils/Common';
import { IReduxState } from '../../store';
import useNavigate from '../../hooks/useNavigate';
import { ILoginEncryptionResDataV1, IUserLoginReqV1 } from '../../api/common.d';
import { sm2EncryptPassword } from '../../utils/sm2Encrypt';

const Login = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { t } = useTranslation();
const location = useLocation();
const encryptionCacheRef = useRef<ILoginEncryptionResDataV1 | null>(null);

const login = (formData: {
const fetchLoginEncryption = async (): Promise<ILoginEncryptionResDataV1> => {
const res = await user.getLoginEncryptionV1();
if (res.data.code !== ResponseCode.SUCCESS || !res.data.data) {
throw new Error('get login encryption failed');

Check warning on line 37 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 38 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
encryptionCacheRef.current = res.data.data;
return res.data.data;
};

const buildLoginBody = async (formData: {
username: string;
password: string;
}): Promise<IUserLoginReqV1> => {
let info = encryptionCacheRef.current;
if (!info) {
info = await fetchLoginEncryption();

Check warning on line 49 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 50 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (info.enable === true) {
if (
info.algorithm !== 'SM2' ||

Check warning on line 54 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
info.cipher_mode !== 'C1C3C2' ||

Check warning on line 55 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
!info.public_key ||

Check warning on line 56 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
!info.key_id

Check warning on line 57 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
) {
throw new Error('invalid login encryption params');

Check warning on line 59 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 60 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 60 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
const encrypted_username = sm2EncryptPassword(
formData.username,
info.public_key
);

Check warning on line 64 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const encrypted_password = sm2EncryptPassword(
formData.password,
info.public_key
);

Check warning on line 68 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return {
encrypted_username,
encrypted_password,
key_id: info.key_id,
};

Check warning on line 73 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 74 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if (info.enable === false) {
return {
username: formData.username,
password: formData.password,
};
}

throw new Error('login encryption enable unknown');

Check warning on line 83 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
};

const login = async (formData: {
username: string;
password: string;
userAgreement: boolean;
Expand All @@ -39,29 +94,31 @@
return;
}
/* FITRUE_isEE */
user
.loginV2({
username: formData.username,
password: formData.password,
})
.then((res) => {
if (res.data.code === ResponseCode.SUCCESS) {
const params = new URLSearchParams(location.search);
dispatch(
updateToken({ token: getCookie(SQLE_COOKIE_TOKEN_KEY_NAME) })
);
const target = params.get(SQLE_REDIRECT_KEY_PARAMS_NAME);
if (target) {
if (target === '/sqlQuery') {
navigate(`sqlQuery?${OPEN_CLOUD_BEAVER_URL_PARAM_NAME}=true`);
} else {
navigate(target);
}
let body: IUserLoginReqV1;
try {
body = await buildLoginBody(formData);
} catch {
message.error(t('login.errorMessage.encryption'));

Check warning on line 101 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return;

Check warning on line 102 in src/page/Login/index.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
user.loginV2(body).then((res) => {
if (res.data.code === ResponseCode.SUCCESS) {
const params = new URLSearchParams(location.search);
dispatch(
updateToken({ token: getCookie(SQLE_COOKIE_TOKEN_KEY_NAME) })
);
const target = params.get(SQLE_REDIRECT_KEY_PARAMS_NAME);
if (target) {
if (target === '/sqlQuery') {
navigate(`sqlQuery?${OPEN_CLOUD_BEAVER_URL_PARAM_NAME}=true`);
} else {
navigate('home');
navigate(target);
}
} else {
navigate('home');
}
});
}
});
};
const { run: getOauth2Tips, data: oauthConfig } = useRequest(
() => configuration.getOauth2Tips().then((res) => res.data?.data ?? {}),
Expand All @@ -75,6 +132,13 @@
webLogoUrl: state.system.webLogoUrl,
}));

useEffect(() => {
fetchLoginEncryption().catch(() => {
encryptionCacheRef.current = null;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

/* IFTRUE_isEE */
useEffect(() => {
getOauth2Tips();
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const UI_VERSION="feature/issue-110 72a0e52"
export const UI_VERSION="feat/yumchina-login-sm2-encryption a855454"
14 changes: 14 additions & 0 deletions src/types/sm-crypto.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
declare module 'sm-crypto' {
export const sm2: {
doEncrypt: (
msg: string,
publicKey: string,
cipherMode?: number
) => string;
doDecrypt: (
encryptData: string,
privateKey: string,
cipherMode?: number
) => string;
};
}
22 changes: 22 additions & 0 deletions src/utils/sm2Encrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { sm2 } from 'sm-crypto';

/** sm-crypto: 1 = C1C3C2(与后端 gmsm 一致) */
const CIPHER_MODE_C1C3C2 = 1;

/**
* 用 SM2 公钥对明文(用户名或口令)做 C1C3C2 加密,返回 hex(无前导 04;服务端会补齐)。
* publicKeyHex 为未压缩点 hex;sm-crypto 要求带前导 04。
*/
export function sm2EncryptPassword(

Check warning on line 10 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
plaintext: string,
publicKeyHex: string
): string {
let key = (publicKeyHex || '').trim();

Check warning on line 14 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 14 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 14 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
if (!key) {
throw new Error('empty sm2 public key');

Check warning on line 16 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 17 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 17 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
if (!/^04/i.test(key)) {
key = `04${key}`;

Check warning on line 19 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 20 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 20 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
return sm2.doEncrypt(plaintext, key, CIPHER_MODE_C1C3C2);

Check warning on line 21 in src/utils/sm2Encrypt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7425,6 +7425,11 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"

jsbn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==

jsdom@^16.6.0:
version "16.7.0"
resolved "https://registry.npmmirror.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
Expand Down Expand Up @@ -10519,6 +10524,13 @@ slash@^4.0.0:
resolved "https://registry.npmmirror.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==

sm-crypto@0.3.13:
version "0.3.13"
resolved "https://registry.yarnpkg.com/sm-crypto/-/sm-crypto-0.3.13.tgz#9615d67f9f2280970c353122e5901ae87d64899a"
integrity sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==
dependencies:
jsbn "^1.1.0"

sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
Expand Down
Loading