Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement the initialization workflow #272

Merged
merged 1 commit into from
Oct 19, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ private UserConfigKey() {}
CONFIG_VALUE_TYPES.put(CHAT_ENABLED, Boolean.class);
CONFIG_VALUE_TYPES.put(CHAT_ENDPOINT, String.class);
CONFIG_VALUE_TYPES.put(ADMIN_PASSWORD_CHANGE_DISABLED, Boolean.class);
CONFIG_VALUE_TYPES.put(SYSTEM_INITIALIZED, Boolean.class);
}

public static Class<?> getConfigValueType(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import com.alibaba.higress.console.service.SessionService;
import com.alibaba.higress.console.service.SystemService;

import javax.annotation.PostConstruct;

/**
* @author CH3CHO
*/
Expand Down Expand Up @@ -67,6 +69,11 @@ public void setSystemService(SystemService systemService) {
this.systemService = systemService;
}

@PostConstruct
public void syncSystemState() {
configService.setConfig(UserConfigKey.SYSTEM_INITIALIZED, sessionService.isAdminInitialized());
}

@PostMapping("/init")
public ResponseEntity<?> initialize(@RequestBody SystemInitRequest request) {
User adminUser = request.getAdminUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
*/
public interface SessionService {

boolean isAdminInitialized();

void initializeAdmin(User user);

User login(String username, String password);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,16 @@ public void setKubernetesClientService(KubernetesClientService kubernetesClientS
this.kubernetesClientService = kubernetesClientService;
}

@Override
public boolean isAdminInitialized() {
return tryGetAdminConfig() != null;
}

@Override
public void initializeAdmin(User user) {
boolean initialized = configService.getBoolean(UserConfigKey.SYSTEM_INITIALIZED, false);
boolean initialized = isAdminInitialized();
if (initialized) {
throw new IllegalStateException("System is already initialized.");
throw new IllegalStateException("Admin user is already initialized.");
}
V1Secret secret;
try {
Expand Down Expand Up @@ -174,7 +179,11 @@ public User validateSession(HttpServletRequest request) {
return null;
}

AdminConfig config = getAdminConfig();
AdminConfig config = tryGetAdminConfig();
if (config == null) {
return null;
}

String rawToken;
try {
rawToken = AesUtil.decrypt(config.getEncryptKey(), config.getEncryptIv(), token);
Expand Down Expand Up @@ -253,17 +262,22 @@ private String generateToken(User user) {
}

private AdminConfig getAdminConfig() {
AdminConfig config = tryGetAdminConfig();
if (config == null) {
throw new IllegalStateException("No valid admin config is available.");
}
return config;
}

private AdminConfig tryGetAdminConfig() {
AdminConfig localAdminConfig = adminConfigCache.get();
if (localAdminConfig == null || localAdminConfig.isExpired(configTtl)) {
if (localAdminConfig == null || !localAdminConfig.isExpired(configTtl)) {
localAdminConfig = loadAdminConfig();
if (localAdminConfig != null) {
if (localAdminConfig != null && localAdminConfig.isValid()) {
localAdminConfig.setLastUpdateTimestamp(System.currentTimeMillis());
adminConfigCache.set(localAdminConfig);
}
}
if (localAdminConfig == null || !localAdminConfig.isValid()) {
throw new IllegalStateException("No valid admin config is available.");
}
return localAdminConfig;
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const SYSTEM_INITIALIZED = 'system.initialized';
export const LOGIN_PROMPT = 'login.prompt';

export const MODE = 'mode';
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/interfaces/system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface UserInfo {
name: string;
displayName: string;
password?: string;
type?: 'user' | 'admin' | 'guest';
avatarUrl?: string;
}

export interface InitParams {
adminUser: UserInfo;
configs?: object;
}
17 changes: 15 additions & 2 deletions frontend/src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,22 @@
"autoLogin": "Auto login",
"forgotPassword": "Forgot password",
"usernamePlaceholder": "Username",
"usernameRequired": "Please input the username!",
"usernameRequired": "Please input username",
"passwordPlaceholder": "Password",
"passwordRequired": "Please input the password!"
"passwordRequired": "Please input password"
},
"init": {
"title": "System Setup",
"header": "Setup Admin Account",
"usernamePlaceholder": "Username",
"usernameRequired": "Please input username",
"passwordPlaceholder": "Password",
"passwordRequired": "Please input password",
"confirmPasswordPlaceholder": "Re-type Password",
"confirmPasswordRequired": "Please re-type the password here",
"confirmPasswordMismatched": "Password does not match. Enter the password again here.",
"initSuccess": "Setup completed. Redirecting to the login page.",
"initFailed": "Setup operation failed."
},
"domain": {
"columns": {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
"index": {
"title": "Higress Console"
},
"init": {
"title": "系统初始化",
"header": "初始化管理员账号",
"usernamePlaceholder": "用户名",
"usernameRequired": "请输入用户名",
"passwordPlaceholder": "密码",
"passwordRequired": "请输入密码",
"confirmPasswordPlaceholder": "确认密码",
"confirmPasswordRequired": "请确认密码",
"confirmPasswordMismatched": "两次输入的密码不一致",
"initSuccess": "初始化成功,稍后将跳转至登录页面。",
"initFailed": "初始化操作失败。"
},
"login": {
"title": "登录",
"buttonText": "登录",
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/pages/_defaultProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export default {
route: {
path: '/',
routes: [
{
name: 'init.title',
path: '/init',
hideFromMenu: true,
usePureLayout: true,
},
{
name: 'login.title',
path: '/login',
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/pages/init/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.language-dropdown {
position: absolute;
right: 90px;
top: 15px;
}

.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
}

.container :global {
.ant-pro-form-login-container {
padding-top: 200px;
}

.ant-pro-form-login-logo {
width: 70px;
height: 70px;
}
}

@media (min-width: 768px) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}

.content {
padding: 32px 0 24px;
}
}
139 changes: 139 additions & 0 deletions frontend/src/pages/init/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import logo from '@/assets/logo.png';
import LanguageDropdown from '@/components/LanguageDropdown';
import { SYSTEM_INITIALIZED } from '@/interfaces/config';
import { UserInfo } from '@/interfaces/system';
import { initialize } from '@/services/system';
import store from '@/store';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-form';
import { message } from 'antd';
import { useNavigate } from 'ice';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './index.module.css';

const Init: React.FC = () => {
const { t } = useTranslation();

const [configModel] = store.useModel('config');
const navigate = useNavigate();
const formRef = useRef(null);

useEffect(() => {
const properties = configModel ? configModel.properties : {};
if (properties[SYSTEM_INITIALIZED]) {
navigate('/', { replace: true });
}
}, [configModel]);

async function handleSubmit(values: UserInfo) {
try {
await initialize({
adminUser: {
name: values.name,
displayName: values.name,
password: values.password,
},
});
message.success(t('init.initSuccess'));
setTimeout(() => {
window.location.href = '/login';
}, 3000);
} catch (error) {
message.error(t('init.initFailed'));
}
}

return (
<div className={styles.container}>
<div className={styles['language-dropdown']}>
<LanguageDropdown />
</div>
<LoginForm
title=""
logo={<img alt="logo" src={logo} />}
subTitle=""
onFinish={async (values) => {
await handleSubmit(values as UserInfo);
}}
submitter={
{
searchConfig: {
submitText: t('misc.submit'),
},
}
}
formRef={formRef}
>
<h3 style={{ textAlign: 'center', marginBottom: '1rem' }}>
{t('init.header')}
</h3>
<ProFormText
name="name"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={'prefixIcon'} />,
}}
placeholder={t('init.usernamePlaceholder') || ''}
initialValue="admin"
rules={[
{
required: true,
message: t('init.usernameRequired') || '',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
placeholder={t('init.passwordPlaceholder') || ''}
rules={[
{
required: true,
message: t('init.passwordRequired') || '',
},
]}
/>
<ProFormText.Password
name="confirmPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
placeholder={t('init.confirmPasswordPlaceholder') || ''}
rules={[
{
required: true,
message: t('init.confirmPasswordRequired') || '',
},
{
validator(rule, value) {
if (!value) {
return Promise.resolve();
}
const password = formRef.current.getFieldValue("password");
if (!password || password === value) {
return Promise.resolve();
}
return Promise.reject(t('init.confirmPasswordMismatched'));
},
},
]}
/>
</LoginForm>
</div>
);
};

export const getConfig = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation();
return {
title: t('init.title'),
};
};

export default Init;
Loading