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 ( +
+
+ +
+ } + subTitle="" + onFinish={async (values) => { + await handleSubmit(values as UserInfo); + }} + submitter={ + { + searchConfig: { + submitText: t('misc.submit'), + }, + } + } + formRef={formRef} + > +

+ {t('init.header')} +

+ , + }} + placeholder={t('init.usernamePlaceholder') || ''} + initialValue="admin" + rules={[ + { + required: true, + message: t('init.usernameRequired') || '', + }, + ]} + /> + , + }} + placeholder={t('init.passwordPlaceholder') || ''} + rules={[ + { + required: true, + message: t('init.passwordRequired') || '', + }, + ]} + /> + , + }} + 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')); + }, + }, + ]} + /> +
+
+ ); +}; + +export const getConfig = () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation(); + return { + title: t('init.title'), + }; +}; + +export default Init; diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx index ede8406c..3ac30650 100644 --- a/frontend/src/pages/login/index.tsx +++ b/frontend/src/pages/login/index.tsx @@ -1,32 +1,17 @@ import logo from '@/assets/logo.png'; import LanguageDropdown from '@/components/LanguageDropdown'; -import { LOGIN_PROMPT } from '@/interfaces/config'; +import { LOGIN_PROMPT, SYSTEM_INITIALIZED } from '@/interfaces/config'; import type { LoginParams, UserInfo } from '@/interfaces/user'; import { login } from '@/services'; import store from '@/store'; import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { LoginForm, ProFormCheckbox, ProFormText } from '@ant-design/pro-form'; import { Alert, message } from 'antd'; -import { history, useAuth } from 'ice'; +import { history, useAuth, useNavigate } from 'ice'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './index.module.css'; -const LoginMessage: React.FC<{ - content: string; -}> = ({ content }) => { - return ( - - ); -}; - const Login: React.FC = () => { const { t } = useTranslation(); @@ -34,9 +19,14 @@ const Login: React.FC = () => { const [, userDispatcher] = store.useModel('user'); const [configModel] = store.useModel('config'); const [, setAuth] = useAuth(); + const navigate = useNavigate(); useEffect(() => { const properties = configModel ? configModel.properties : {}; + if (!properties[SYSTEM_INITIALIZED]) { + navigate('/init', { replace: true }); + return; + } setLoginPrompt(properties[LOGIN_PROMPT]); }, [configModel]); @@ -47,8 +37,6 @@ const Login: React.FC = () => { async function handleSubmit(values: LoginParams) { try { const user = await login(values); - // eslint-disable-next-line no-console - console.log(user); // We only support admin role at the moment. user.type = 'admin'; message.success(t('login.loginSuccess')); @@ -66,8 +54,6 @@ const Login: React.FC = () => { return; } catch (error) { message.error(t('login.loginFailed')); - // eslint-disable-next-line no-console - console.log(error); } } return ( diff --git a/frontend/src/services/request.tsx b/frontend/src/services/request.tsx index 8d7a843f..3c49ce66 100644 --- a/frontend/src/services/request.tsx +++ b/frontend/src/services/request.tsx @@ -50,7 +50,7 @@ request.interceptors.response.use( } // Unauthorized. Jump to the login page. Promise.reject(error); - if (window.location.href.indexOf('/login') === -1) { + if (window.location.href.indexOf('/init') === -1 && window.location.href.indexOf('/login') === -1) { window.location.href = `/login?redirect=${window.location.pathname}`; } return; diff --git a/frontend/src/services/system.ts b/frontend/src/services/system.ts index a102cfd4..b4655346 100644 --- a/frontend/src/services/system.ts +++ b/frontend/src/services/system.ts @@ -1,3 +1,4 @@ +import { InitParams } from '@/interfaces/system'; import request from './request'; export async function getSystemInfo(): Promise { @@ -8,6 +9,6 @@ export async function getConfigs(): Promise { return await request.get('/system/config'); } -export async function initialize(payload: any): Promise { +export async function initialize(payload: InitParams): Promise { return request.post('/system/init', payload); } diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt index bb6edebd..73379261 100644 --- a/helm/templates/NOTES.txt +++ b/helm/templates/NOTES.txt @@ -1,14 +1,8 @@ -1. Use the following URL to access the console: +Use the following URL to access the console: {{- range $path := .Values.ingress.paths }} http{{ if $.Values.tlsSecretName }}s{{ end }}://{{ $.Values.domain }}{{ .path }} {{- end }} {{- if or .Values.global.local .Values.global.kind }} Since Higress Console is running in local mode, you may need to add the following line into your hosts file before accessing the console: 127.0.0.1 {{ $.Values.domain }} -{{- end }} -2. Use following commands to get the credential and login: - export ADMIN_USERNAME=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ include "higress-console.name" . }} -o jsonpath="{.data.adminUsername}" | base64 -d) - export ADMIN_PASSWORD=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ include "higress-console.name" . }} -o jsonpath="{.data.adminPassword}" | base64 -d) - echo -e "Username: ${ADMIN_USERNAME}\nPassword: ${ADMIN_PASSWORD}" - NOTE: If this is an upgrade release, your current password won't be changed. -3. If you'd like to change the credential, you can edit this secret with new values: {{ .Release.Namespace }}/{{ include "higress-console.name" . }} \ No newline at end of file +{{- end }} \ No newline at end of file diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml index 74630f0c..1a250a6e 100644 --- a/helm/templates/secret.yaml +++ b/helm/templates/secret.yaml @@ -1,4 +1,5 @@ {{- $existedSecret := (lookup "v1" "Secret" .Release.Namespace (include "higress-console.name" .)) }} +{{- $password := .Values.admin.password }} apiVersion: v1 kind: Secret metadata: @@ -6,26 +7,15 @@ metadata: namespace: {{ .Release.Namespace }} type: Opaque data: +{{- if $existedSecret }} + {{- range $k, $v := $existedSecret.data }} + {{ $k }}: {{ $v }} + {{- end}} +{{- else if $password }} + # Only initialize the secret if user sets the password explictly. adminUsername: {{ .Values.admin.username | b64enc }} adminDisplayName: {{ .Values.admin.displayName | b64enc }} -{{- $password := .Values.admin.password.value }} -{{- $key := "" }} -{{- $iv := "" }} -{{- if $existedSecret }} -{{- $password = $existedSecret.data.adminPassword | b64dec }} -{{- $key = $existedSecret.data.key }} -{{- $iv = $existedSecret.data.iv }} -{{- end}} -{{- if not $password }} -{{- $passwordLength := int (default 8 .Values.admin.password.length) }} -{{- $password = randAlphaNum $passwordLength | nospace }} -{{- end }} -{{- if not $key }} -{{- $key = randAscii 32 | b64enc }} -{{- end }} -{{- if not $iv }} -{{- $iv = randAscii 16 | b64enc }} -{{- end }} adminPassword: {{ $password | b64enc }} - key: {{ $key }} - iv: {{ $iv }} + key: {{ randAscii 32 | b64enc }} + iv: {{ randAscii 16 | b64enc }} +{{- end}} diff --git a/helm/values.yaml b/helm/values.yaml index 900af516..d3917d17 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -52,9 +52,7 @@ web: admin: username: admin displayName: Admin - password: - length: 8 # The length of random password generated during installation - value: "" # If set, the value will be used as the admin password for login, and Helm won't generate a random password. + password: "" # If set, the value will be used as the admin password for login. chat: enabled: false