Copilot commented on code in PR #864:
URL: https://github.com/apache/dubbo-go-pixiu/pull/864#discussion_r2697366117


##########
admin/web/src/components/DualModeEditor/index.tsx:
##########
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { Segmented, Form, Input, InputNumber, Select, theme } from 'antd';
+import { useTranslation } from 'react-i18next';
+import Editor, { type Monaco } from '@monaco-editor/react';
+import { configureMonacoYaml } from 'monaco-yaml';
+import { useThemeStore } from '../../stores/theme';
+import type { FormInstance } from 'antd';
+
+export type EditorMode = 'form' | 'yaml';
+
+export interface FieldConfig {
+  name: string;
+  label: string;
+  type: 'input' | 'number' | 'select' | 'textarea';
+  required?: boolean;
+  options?: { label: string; value: string | number | boolean }[];
+  placeholder?: string;
+  min?: number;
+  max?: number;
+  disabled?: boolean;  // Always disabled regardless of readOnly
+  hidden?: boolean;    // Hide this field
+}
+
+interface DualModeEditorProps<T = unknown> {
+  mode: EditorMode;
+  onModeChange: (mode: EditorMode) => void;
+  form: FormInstance<T>;
+  yamlValue: string;
+  onYamlChange: (value: string) => void;
+  fields: FieldConfig[];
+  formToYaml: (data: T, existingYaml: string) => string;
+  yamlToForm: (yaml: string) => T | null;
+  readOnly?: boolean;
+}
+
+export function DualModeEditor<T>({
+  mode,
+  onModeChange,
+  form,
+  yamlValue,
+  onYamlChange,
+  fields,
+  formToYaml,
+  yamlToForm,
+  readOnly = false,
+}: DualModeEditorProps<T>) {
+  const { t } = useTranslation();
+  const { isDark } = useThemeStore();
+  const { token } = theme.useToken();
+  const [syncError, setSyncError] = useState<string | null>(null);
+  const monacoConfigured = useRef(false);
+
+  const handleEditorBeforeMount = useCallback((monaco: Monaco) => {
+    if (monacoConfigured.current) return;
+    monacoConfigured.current = true;
+
+    configureMonacoYaml(monaco, {
+      enableSchemaRequest: false,
+      validate: true,
+      format: true,
+      hover: true,
+      completion: true,
+    });
+  }, []);
+
+  const handleModeChange = useCallback(
+    (newMode: EditorMode) => {
+      if (newMode === mode) return;
+
+      if (newMode === 'yaml') {
+        const formData = form.getFieldsValue() as T;
+        const yaml = formToYaml(formData, yamlValue);
+        onYamlChange(yaml);
+      } else {
+        const formData = yamlToForm(yamlValue);
+        if (formData) {
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          form.setFieldsValue(formData as any);
+          setSyncError(null);
+        } else {
+          setSyncError(t('common.yamlParseError'));
+          return;
+        }
+      }
+      onModeChange(newMode);
+    },
+    [mode, form, yamlValue, formToYaml, yamlToForm, onYamlChange, 
onModeChange, t]
+  );
+
+  useEffect(() => {
+    // Clear sync error when yaml value changes externally
+    // eslint-disable-next-line react-hooks/set-state-in-effect -- resetting 
error state on prop change
+    setSyncError(null);

Review Comment:
   Setting state directly in useEffect based on prop changes can lead to 
unnecessary re-renders. Consider using a ref or restructuring to avoid the 
eslint override. If the current approach is intentional, the comment 
justification is acceptable.



##########
admin/internal/store/mysql.go:
##########
@@ -0,0 +1,733 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package store
+
+import (
+       "crypto/md5"
+       "encoding/hex"
+       "fmt"
+)
+
+import (
+       "github.com/pkg/errors"
+
+       "golang.org/x/crypto/bcrypt"
+
+       "gorm.io/driver/mysql"
+
+       "gorm.io/gorm"
+       "gorm.io/gorm/logger"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/admin/internal/model"
+       "github.com/apache/dubbo-go-pixiu/admin/pkg/config"
+)
+
+// MySQL handles all MySQL database operations using GORM.
+type MySQL struct {
+       db *gorm.DB
+}
+
+// NewMySQL creates a new MySQL store with GORM.
+func NewMySQL(cfg config.MySQLConfig) (*MySQL, error) {
+       dsn := 
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+               cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
+
+       db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+               Logger: logger.Default.LogMode(logger.Info),
+       })
+       if err != nil {
+               return nil, errors.Wrap(err, "failed to connect to mysql")
+       }
+
+       // Auto migrate tables
+       if err := db.AutoMigrate(
+               &model.User{},
+               &model.Role{},
+               &model.UserRole{},
+               &model.Permission{},
+               &model.RolePermission{},
+       ); err != nil {
+               return nil, errors.Wrap(err, "failed to migrate database")
+       }
+
+       m := &MySQL{db: db}
+
+       // Initialize default data
+       if err := m.initDefaultData(); err != nil {
+               return nil, errors.Wrap(err, "failed to initialize default 
data")
+       }
+
+       // Initialize default permissions
+       if err := m.InitDefaultPermissions(); err != nil {
+               return nil, errors.Wrap(err, "failed to initialize default 
permissions")
+       }
+
+       return m, nil
+}
+
+// initDefaultData creates default admin user and roles if they don't exist.
+func (m *MySQL) initDefaultData() error {
+       // Create default admin role
+       adminRole := model.Role{ID: 1, RoleName: "admin", Description: 
"Administrator"}
+       if err := m.db.FirstOrCreate(&adminRole, model.Role{ID: 1}).Error; err 
!= nil {
+               return err
+       }
+
+       // Create default user role
+       userRole := model.Role{ID: 2, RoleName: "user", Description: "Normal 
User"}
+       if err := m.db.FirstOrCreate(&userRole, model.Role{ID: 2}).Error; err 
!= nil {
+               return err
+       }
+
+       // Create default admin user if not exists (password: admin)
+       // Note: Only create if not exists, don't reset password on every 
startup
+       var adminUser model.User
+       result := m.db.Where("username = ?", "admin").First(&adminUser)
+       if result.Error != nil {
+               if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+                       // Create new admin user with bcrypt password
+                       hashedPassword, err := hashPassword("admin")
+                       if err != nil {
+                               return errors.Wrap(err, "failed to hash default 
admin password")
+                       }
+                       adminUser = model.User{
+                               Username: "admin",
+                               Password: hashedPassword,
+                               Role:     1,
+                               Enabled:  true,
+                       }
+                       if err := m.db.Create(&adminUser).Error; err != nil {
+                               return err
+                       }
+               } else {
+                       return result.Error
+               }
+       }
+       // Note: Removed automatic password reset on startup for security
+
+       // Assign admin role to admin user
+       userRoleAssign := model.UserRole{UserID: adminUser.ID, RoleID: 1}
+       if err := m.db.FirstOrCreate(&userRoleAssign, model.UserRole{UserID: 
adminUser.ID}).Error; err != nil {
+               return err
+       }
+
+       return nil
+}
+
+// Close closes the database connection.
+func (m *MySQL) Close() error {
+       sqlDB, err := m.db.DB()
+       if err != nil {
+               return err
+       }
+       return sqlDB.Close()
+}
+
+// DB returns the underlying GORM DB instance.
+func (m *MySQL) DB() *gorm.DB {
+       return m.db
+}
+
+// --- User Operations ---
+
+// Login validates user credentials and returns user ID if successful.
+// Supports automatic migration from legacy MD5 hashes to bcrypt.
+func (m *MySQL) Login(username, password string) (uint, error) {
+       var user model.User
+       err := m.db.Where("username = ?", username).First(&user).Error
+       if errors.Is(err, gorm.ErrRecordNotFound) {
+               return 0, errors.New("invalid username or password")
+       }
+       if err != nil {
+               return 0, errors.Wrap(err, "failed to query user")
+       }
+
+       if !user.Enabled {
+               return 0, errors.New("user is disabled")
+       }
+
+       // Check password
+       if !checkPassword(password, user.Password) {
+               // If it's a legacy MD5 hash, try MD5 comparison for migration
+               if isLegacyMD5Hash(user.Password) {
+                       if !checkLegacyMD5Password(password, user.Password) {
+                               return 0, errors.New("invalid username or 
password")
+                       }
+                       // Migrate to bcrypt (ignore error - password will be 
upgraded on next login)
+                       _ = m.upgradePasswordHash(user.ID, password)
+               } else {
+                       return 0, errors.New("invalid username or password")
+               }
+       }
+
+       return user.ID, nil
+}
+
+// checkLegacyMD5Password checks password against legacy MD5 hash.
+func checkLegacyMD5Password(password, hash string) bool {
+       md5Hash := md5.Sum([]byte(password))
+       return hex.EncodeToString(md5Hash[:]) == hash
+}
+
+// upgradePasswordHash upgrades a user's password hash from MD5 to bcrypt.
+func (m *MySQL) upgradePasswordHash(userID uint, password string) error {
+       newHash, err := hashPassword(password)
+       if err != nil {
+               return err
+       }
+       return m.db.Model(&model.User{}).Where("id = ?", 
userID).Update("password", newHash).Error
+}
+
+// Register creates a new user.
+func (m *MySQL) Register(username, password string) error {
+       if username == "" {
+               return errors.New("username cannot be empty")
+       }
+
+       // Check if user already exists
+       var count int64
+       m.db.Model(&model.User{}).Where("username = ?", username).Count(&count)
+       if count > 0 {
+               return errors.New("user already exists")
+       }
+
+       // Hash password
+       hashedPassword, err := hashPassword(password)
+       if err != nil {
+               return errors.Wrap(err, "failed to hash password")
+       }
+
+       // Create user
+       user := model.User{
+               Username: username,
+               Password: hashedPassword,
+               Role:     0,
+               Enabled:  true,
+       }
+       if err := m.db.Create(&user).Error; err != nil {
+               return errors.Wrap(err, "failed to create user")
+       }
+
+       // Assign default user role
+       userRole := model.UserRole{UserID: user.ID, RoleID: 2}
+       if err := m.db.Create(&userRole).Error; err != nil {
+               return errors.Wrap(err, "failed to assign user role")
+       }
+
+       return nil
+}
+
+// GetUserInfo returns user information by username.
+func (m *MySQL) GetUserInfo(username string) (*model.UserInfo, error) {
+       var user model.User
+       err := m.db.Where("username = ?", username).First(&user).Error
+       if errors.Is(err, gorm.ErrRecordNotFound) {
+               return nil, errors.New("user not found")
+       }
+       if err != nil {
+               return nil, errors.Wrap(err, "failed to get user info")
+       }
+
+       return &model.UserInfo{
+               ID:       user.ID,
+               Username: user.Username,
+               Role:     user.Role,
+       }, nil
+}
+
+// GetUserRole returns the user's role information.
+func (m *MySQL) GetUserRole(username string) (*model.Role, error) {
+       var role model.Role
+       err := m.db.
+               Joins("JOIN pixiu_user_role ur ON pixiu_role.id = ur.role_id").
+               Joins("JOIN pixiu_user u ON u.id = ur.user_id").
+               Where("u.username = ?", username).
+               First(&role).Error
+
+       if errors.Is(err, gorm.ErrRecordNotFound) {
+               return nil, errors.New("role not found")
+       }
+       if err != nil {
+               return nil, errors.Wrap(err, "failed to get user role")
+       }
+
+       return &role, nil
+}
+
+// IsAdmin checks if the user is an administrator.
+func (m *MySQL) IsAdmin(username string) (bool, error) {
+       var count int64
+       err := m.db.Model(&model.User{}).
+               Where("username = ? AND role = 1", username).
+               Count(&count).Error
+
+       if err != nil {
+               return false, errors.Wrap(err, "failed to check admin status")
+       }
+
+       return count > 0, nil
+}
+
+// ChangePassword updates the user's password.
+func (m *MySQL) ChangePassword(username, oldPassword, newPassword string) 
error {
+       // Get user first
+       var user model.User
+       err := m.db.Where("username = ?", username).First(&user).Error
+       if errors.Is(err, gorm.ErrRecordNotFound) {
+               return errors.New("user not found")
+       }
+       if err != nil {
+               return errors.Wrap(err, "failed to query user")
+       }
+
+       // Verify old password (support both bcrypt and legacy MD5)
+       passwordValid := checkPassword(oldPassword, user.Password)
+       if !passwordValid && isLegacyMD5Hash(user.Password) {
+               passwordValid = checkLegacyMD5Password(oldPassword, 
user.Password)
+       }
+       if !passwordValid {
+               return errors.New("invalid old password")
+       }
+
+       // Hash new password with bcrypt
+       newHashed, err := hashPassword(newPassword)
+       if err != nil {
+               return errors.Wrap(err, "failed to hash new password")
+       }
+
+       // Update password
+       result := m.db.Model(&model.User{}).
+               Where("id = ?", user.ID).
+               Update("password", newHashed)
+
+       if result.Error != nil {
+               return errors.Wrap(result.Error, "failed to update password")
+       }
+
+       return nil
+}
+
+// hashPassword hashes a password using bcrypt.
+func hashPassword(password string) (string, error) {
+       bytes, err := bcrypt.GenerateFromPassword([]byte(password), 
bcrypt.DefaultCost)
+       if err != nil {
+               return "", errors.Wrap(err, "failed to hash password")
+       }
+       return string(bytes), nil
+}
+
+// checkPassword verifies a password against a hash.
+// Supports both bcrypt (new) and legacy MD5 hashes for migration.
+func checkPassword(password, hash string) bool {

Review Comment:
   The function `checkPassword` is defined at line 328 but used at line 167. 
Consider moving the helper functions (`hashPassword`, `checkPassword`, 
`isLegacyMD5Hash`, `checkLegacyMD5Password`) to the top of the file or to a 
separate helpers section for better code organization.



##########
admin/web/eslint.config.js:
##########
@@ -14,33 +14,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export const menuList = [{
-  name: '网关配置',
-  id: 'Gateway',
-  children: [{
-    name: '概览',
-    id: 'Overview',
-    componentName: '/Overview'
-  },
+
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+  globalIgnores(['dist']),
   {
-    name: '插件配置',
-    id: 'Plug',
-    componentName: 'Plug'
-  },{
-    name: '集群管理',
-    id: 'Cluster',
-    componentName: 'Cluster'
-  },{
-    name: 'Listener管理',
-    id: 'Listener',
-    componentName: 'Listener'
-  }]
-}, {
-  name: '限流配置',
-  id: 'Flow',
-  children: [{
-    name: '限流配置',
-    id: 'RateLimiter',
-    componentName: '/RateLimiter'
-  }]
-}]
+    files: ['**/*.{ts,tsx}'],
+    extends: [
+      js.configs.recommended,
+      tseslint.configs.recommended,

Review Comment:
   The eslint config uses `tseslint.configs.recommended` without importing it 
from a configs object. Verify this is the correct API for typescript-eslint v8. 
The typical import is `...tseslint.configs.recommended` when extending configs.
   ```suggestion
         ...tseslint.configs.recommended,
   ```



##########
admin/internal/store/mysql.go:
##########
@@ -0,0 +1,733 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package store
+
+import (
+       "crypto/md5"
+       "encoding/hex"
+       "fmt"
+)
+
+import (
+       "github.com/pkg/errors"
+
+       "golang.org/x/crypto/bcrypt"
+
+       "gorm.io/driver/mysql"
+
+       "gorm.io/gorm"
+       "gorm.io/gorm/logger"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/admin/internal/model"
+       "github.com/apache/dubbo-go-pixiu/admin/pkg/config"
+)
+
+// MySQL handles all MySQL database operations using GORM.
+type MySQL struct {
+       db *gorm.DB
+}
+
+// NewMySQL creates a new MySQL store with GORM.
+func NewMySQL(cfg config.MySQLConfig) (*MySQL, error) {
+       dsn := 
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+               cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
+
+       db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+               Logger: logger.Default.LogMode(logger.Info),
+       })
+       if err != nil {
+               return nil, errors.Wrap(err, "failed to connect to mysql")
+       }
+
+       // Auto migrate tables
+       if err := db.AutoMigrate(
+               &model.User{},
+               &model.Role{},
+               &model.UserRole{},
+               &model.Permission{},
+               &model.RolePermission{},
+       ); err != nil {
+               return nil, errors.Wrap(err, "failed to migrate database")
+       }
+
+       m := &MySQL{db: db}
+
+       // Initialize default data
+       if err := m.initDefaultData(); err != nil {
+               return nil, errors.Wrap(err, "failed to initialize default 
data")
+       }
+
+       // Initialize default permissions
+       if err := m.InitDefaultPermissions(); err != nil {
+               return nil, errors.Wrap(err, "failed to initialize default 
permissions")
+       }
+
+       return m, nil
+}
+
+// initDefaultData creates default admin user and roles if they don't exist.
+func (m *MySQL) initDefaultData() error {
+       // Create default admin role
+       adminRole := model.Role{ID: 1, RoleName: "admin", Description: 
"Administrator"}
+       if err := m.db.FirstOrCreate(&adminRole, model.Role{ID: 1}).Error; err 
!= nil {
+               return err
+       }
+
+       // Create default user role
+       userRole := model.Role{ID: 2, RoleName: "user", Description: "Normal 
User"}
+       if err := m.db.FirstOrCreate(&userRole, model.Role{ID: 2}).Error; err 
!= nil {
+               return err
+       }
+
+       // Create default admin user if not exists (password: admin)
+       // Note: Only create if not exists, don't reset password on every 
startup
+       var adminUser model.User
+       result := m.db.Where("username = ?", "admin").First(&adminUser)
+       if result.Error != nil {
+               if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+                       // Create new admin user with bcrypt password
+                       hashedPassword, err := hashPassword("admin")
+                       if err != nil {
+                               return errors.Wrap(err, "failed to hash default 
admin password")
+                       }
+                       adminUser = model.User{
+                               Username: "admin",
+                               Password: hashedPassword,
+                               Role:     1,
+                               Enabled:  true,
+                       }
+                       if err := m.db.Create(&adminUser).Error; err != nil {
+                               return err
+                       }
+               } else {
+                       return result.Error
+               }
+       }
+       // Note: Removed automatic password reset on startup for security

Review Comment:
   This comment references removed code that isn't visible in the diff. While 
the comment provides context, it may confuse future readers. Consider removing 
it or clarifying that this is a design decision rather than a reference to 
deleted code.
   ```suggestion
        // Note: For security reasons, the admin password is not automatically 
reset on startup
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to