diff --git a/backend/src/main/java/com/alibaba/higress/console/constant/UserConfigKey.java b/backend/src/main/java/com/alibaba/higress/console/constant/UserConfigKey.java index 730c7a8f..61d7ffda 100644 --- a/backend/src/main/java/com/alibaba/higress/console/constant/UserConfigKey.java +++ b/backend/src/main/java/com/alibaba/higress/console/constant/UserConfigKey.java @@ -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) { diff --git a/backend/src/main/java/com/alibaba/higress/console/controller/SystemController.java b/backend/src/main/java/com/alibaba/higress/console/controller/SystemController.java index 14f1554f..80172bbf 100644 --- a/backend/src/main/java/com/alibaba/higress/console/controller/SystemController.java +++ b/backend/src/main/java/com/alibaba/higress/console/controller/SystemController.java @@ -40,6 +40,8 @@ import com.alibaba.higress.console.service.SessionService; import com.alibaba.higress.console.service.SystemService; +import javax.annotation.PostConstruct; + /** * @author CH3CHO */ @@ -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(); diff --git a/backend/src/main/java/com/alibaba/higress/console/service/SessionService.java b/backend/src/main/java/com/alibaba/higress/console/service/SessionService.java index 0110eab0..033ffb8e 100644 --- a/backend/src/main/java/com/alibaba/higress/console/service/SessionService.java +++ b/backend/src/main/java/com/alibaba/higress/console/service/SessionService.java @@ -22,6 +22,8 @@ */ public interface SessionService { + boolean isAdminInitialized(); + void initializeAdmin(User user); User login(String username, String password); diff --git a/backend/src/main/java/com/alibaba/higress/console/service/SessionServiceImpl.java b/backend/src/main/java/com/alibaba/higress/console/service/SessionServiceImpl.java index 08bd2530..25337748 100644 --- a/backend/src/main/java/com/alibaba/higress/console/service/SessionServiceImpl.java +++ b/backend/src/main/java/com/alibaba/higress/console/service/SessionServiceImpl.java @@ -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 { @@ -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); @@ -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; } diff --git a/frontend/src/interfaces/config.ts b/frontend/src/interfaces/config.ts index 4c025ef5..50a6c610 100644 --- a/frontend/src/interfaces/config.ts +++ b/frontend/src/interfaces/config.ts @@ -1,3 +1,4 @@ +export const SYSTEM_INITIALIZED = 'system.initialized'; export const LOGIN_PROMPT = 'login.prompt'; export const MODE = 'mode'; diff --git a/frontend/src/interfaces/system.ts b/frontend/src/interfaces/system.ts new file mode 100644 index 00000000..ce12645d --- /dev/null +++ b/frontend/src/interfaces/system.ts @@ -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; +} diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index 9dcd438e..129faa09 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -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": { diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index fde5d177..399b46b3 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -20,6 +20,19 @@ "index": { "title": "Higress Console" }, + "init": { + "title": "系统初始化", + "header": "初始化管理员账号", + "usernamePlaceholder": "用户名", + "usernameRequired": "请输入用户名", + "passwordPlaceholder": "密码", + "passwordRequired": "请输入密码", + "confirmPasswordPlaceholder": "确认密码", + "confirmPasswordRequired": "请确认密码", + "confirmPasswordMismatched": "两次输入的密码不一致", + "initSuccess": "初始化成功,稍后将跳转至登录页面。", + "initFailed": "初始化操作失败。" + }, "login": { "title": "登录", "buttonText": "登录", diff --git a/frontend/src/pages/_defaultProps.tsx b/frontend/src/pages/_defaultProps.tsx index cffb4ad9..dfedd8f1 100644 --- a/frontend/src/pages/_defaultProps.tsx +++ b/frontend/src/pages/_defaultProps.tsx @@ -12,6 +12,12 @@ export default { route: { path: '/', routes: [ + { + name: 'init.title', + path: '/init', + hideFromMenu: true, + usePureLayout: true, + }, { name: 'login.title', path: '/login', diff --git a/frontend/src/pages/init/index.module.css b/frontend/src/pages/init/index.module.css new file mode 100644 index 00000000..4a9e8079 --- /dev/null +++ b/frontend/src/pages/init/index.module.css @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/pages/init/index.tsx b/frontend/src/pages/init/index.tsx new file mode 100644 index 00000000..1d73e0d0 --- /dev/null +++ b/frontend/src/pages/init/index.tsx @@ -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 ( +