This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch test
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 93e183be625bf4b823068e8108d8a07f79c46f9e
Author: Sonui <[email protected]>
AuthorDate: Tue Oct 28 01:16:21 2025 +0800

    feat: Add resetPassword cli tool
---
 cmd/command.go                         |  65 ++++++--
 cmd/main.go                            |   4 +-
 go.mod                                 |   1 +
 go.sum                                 |   2 +
 internal/base/conf/conf.go             |   4 +-
 internal/base/path/path.go             |  53 ++++++
 internal/cli/install.go                |  44 +----
 internal/cli/install_check.go          |   3 +-
 internal/cli/reset_password.go         | 288 +++++++++++++++++++++++++++++++++
 internal/install/install_controller.go |   9 +-
 internal/install/install_main.go       |   4 +-
 pkg/checker/reserved_username.go       |   4 +-
 12 files changed, 417 insertions(+), 64 deletions(-)

diff --git a/cmd/command.go b/cmd/command.go
index 7d779893..ca01aa80 100644
--- a/cmd/command.go
+++ b/cmd/command.go
@@ -20,11 +20,13 @@
 package answercmd
 
 import (
+       "context"
        "fmt"
        "os"
        "strings"
 
        "github.com/apache/answer/internal/base/conf"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/cli"
        "github.com/apache/answer/internal/install"
        "github.com/apache/answer/internal/migrations"
@@ -53,6 +55,10 @@ var (
        i18nSourcePath string
        // i18nTargetPath i18n to path
        i18nTargetPath string
+       // resetPasswordEmail user email for password reset
+       resetPasswordEmail string
+       // resetPasswordPassword new password for password reset
+       resetPasswordPassword string
 )
 
 func init() {
@@ -76,7 +82,10 @@ func init() {
 
        i18nCmd.Flags().StringVarP(&i18nTargetPath, "target", "t", "", "i18n 
target path, eg: -t ./i18n/target")
 
-       for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, 
dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd} {
+       resetPasswordCmd.Flags().StringVarP(&resetPasswordEmail, "email", "e", 
"", "user email address")
+       resetPasswordCmd.Flags().StringVarP(&resetPasswordPassword, "password", 
"p", "", "new password (not recommended, will be recorded in shell history)")
+
+       for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, 
dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd, resetPasswordCmd} 
{
                rootCmd.AddCommand(cmd)
        }
 }
@@ -96,8 +105,8 @@ To run answer, use:
                Short: "Run Answer",
                Long:  `Start running Answer`,
                Run: func(_ *cobra.Command, _ []string) {
-                       cli.FormatAllPath(dataDirPath)
-                       fmt.Println("config file path: ", 
cli.GetConfigFilePath())
+                       path.FormatAllPath(dataDirPath)
+                       fmt.Println("config file path: ", 
path.GetConfigFilePath())
                        fmt.Println("Answer is 
starting..........................")
                        runApp()
                },
@@ -111,10 +120,10 @@ To run answer, use:
                        // check config file and database. if config file 
exists and database is already created, init done
                        cli.InstallAllInitialEnvironment(dataDirPath)
 
-                       configFileExist := 
cli.CheckConfigFile(cli.GetConfigFilePath())
+                       configFileExist := 
cli.CheckConfigFile(path.GetConfigFilePath())
                        if configFileExist {
                                fmt.Println("config file exists, try to read 
the config...")
-                               c, err := 
conf.ReadConfig(cli.GetConfigFilePath())
+                               c, err := 
conf.ReadConfig(path.GetConfigFilePath())
                                if err != nil {
                                        fmt.Println("read config failed: ", 
err.Error())
                                        return
@@ -128,7 +137,7 @@ To run answer, use:
                        }
 
                        // start installation server to install
-                       install.Run(cli.GetConfigFilePath())
+                       install.Run(path.GetConfigFilePath())
                },
        }
 
@@ -138,9 +147,9 @@ To run answer, use:
                Long:  `Upgrade Answer to the latest version`,
                Run: func(_ *cobra.Command, _ []string) {
                        log.SetLogger(log.NewStdLogger(os.Stdout))
-                       cli.FormatAllPath(dataDirPath)
+                       path.FormatAllPath(dataDirPath)
                        cli.InstallI18nBundle(true)
-                       c, err := conf.ReadConfig(cli.GetConfigFilePath())
+                       c, err := conf.ReadConfig(path.GetConfigFilePath())
                        if err != nil {
                                fmt.Println("read config failed: ", err.Error())
                                return
@@ -159,8 +168,8 @@ To run answer, use:
                Long:  `Back up database into an SQL file`,
                Run: func(_ *cobra.Command, _ []string) {
                        fmt.Println("Answer is backing up data")
-                       cli.FormatAllPath(dataDirPath)
-                       c, err := conf.ReadConfig(cli.GetConfigFilePath())
+                       path.FormatAllPath(dataDirPath)
+                       c, err := conf.ReadConfig(path.GetConfigFilePath())
                        if err != nil {
                                fmt.Println("read config failed: ", err.Error())
                                return
@@ -179,9 +188,9 @@ To run answer, use:
                Short: "Check the required environment",
                Long:  `Check if the current environment meets the startup 
requirements`,
                Run: func(_ *cobra.Command, _ []string) {
-                       cli.FormatAllPath(dataDirPath)
+                       path.FormatAllPath(dataDirPath)
                        fmt.Println("Start checking the required 
environment...")
-                       if cli.CheckConfigFile(cli.GetConfigFilePath()) {
+                       if cli.CheckConfigFile(path.GetConfigFilePath()) {
                                fmt.Println("config file exists [✔]")
                        } else {
                                fmt.Println("config file not exists [x]")
@@ -193,7 +202,7 @@ To run answer, use:
                                fmt.Println("upload directory not exists [x]")
                        }
 
-                       c, err := conf.ReadConfig(cli.GetConfigFilePath())
+                       c, err := conf.ReadConfig(path.GetConfigFilePath())
                        if err != nil {
                                fmt.Println("read config failed: ", err.Error())
                                return
@@ -246,9 +255,9 @@ To run answer, use:
                Short: "Set some config to default value",
                Long:  `Set some config to default value`,
                Run: func(_ *cobra.Command, _ []string) {
-                       cli.FormatAllPath(dataDirPath)
+                       path.FormatAllPath(dataDirPath)
 
-                       c, err := conf.ReadConfig(cli.GetConfigFilePath())
+                       c, err := conf.ReadConfig(path.GetConfigFilePath())
                        if err != nil {
                                fmt.Println("read config failed: ", err.Error())
                                return
@@ -297,6 +306,32 @@ To run answer, use:
                        }
                },
        }
+
+       resetPasswordCmd = &cobra.Command{
+               Use:     "passwd",
+               Aliases: []string{"password", "reset-password"},
+               Short:   "Reset user password",
+               Long:    "Reset user password by email address.",
+               Example: `  # Interactive mode (recommended, safest)
+  answer passwd -C ./answer-data
+
+  # Specify email only (will prompt for password securely)
+  answer passwd -C ./answer-data --email [email protected]
+  answer passwd -C ./answer-data -e [email protected]
+
+  # Specify email and password (NOT recommended, will be recorded in shell 
history)
+  answer passwd -C ./answer-data -e [email protected] -p newpassword123`,
+               Run: func(cmd *cobra.Command, args []string) {
+                       opts := &cli.ResetPasswordOptions{
+                               Email:    resetPasswordEmail,
+                               Password: resetPasswordPassword,
+                       }
+                       if err := cli.ResetPassword(context.Background(), 
dataDirPath, opts); err != nil {
+                               fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+                               os.Exit(1)
+                       }
+               },
+       }
 )
 
 // Execute adds all child commands to the root command and sets flags 
appropriately.
diff --git a/cmd/main.go b/cmd/main.go
index 35256c07..f166d230 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -28,7 +28,7 @@ import (
        "github.com/apache/answer/internal/base/conf"
        "github.com/apache/answer/internal/base/constant"
        "github.com/apache/answer/internal/base/cron"
-       "github.com/apache/answer/internal/cli"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/schema"
        "github.com/gin-gonic/gin"
        "github.com/segmentfault/pacman"
@@ -67,7 +67,7 @@ func Main() {
 }
 
 func runApp() {
-       c, err := conf.ReadConfig(cli.GetConfigFilePath())
+       c, err := conf.ReadConfig(path.GetConfigFilePath())
        if err != nil {
                panic(err)
        }
diff --git a/go.mod b/go.mod
index 6578d5ce..b9fa70eb 100644
--- a/go.mod
+++ b/go.mod
@@ -60,6 +60,7 @@ require (
        golang.org/x/crypto v0.36.0
        golang.org/x/image v0.20.0
        golang.org/x/net v0.38.0
+       golang.org/x/term v0.30.0
        golang.org/x/text v0.23.0
        gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
        gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index afe9b1ea..725870fc 100644
--- a/go.sum
+++ b/go.sum
@@ -785,6 +785,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod 
h1:Nr5EML6q2oocZ2LXR
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod 
h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go
index 4b71b206..db83862b 100644
--- a/internal/base/conf/conf.go
+++ b/internal/base/conf/conf.go
@@ -25,9 +25,9 @@ import (
        "path/filepath"
 
        "github.com/apache/answer/internal/base/data"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/base/server"
        "github.com/apache/answer/internal/base/translator"
-       "github.com/apache/answer/internal/cli"
        "github.com/apache/answer/internal/router"
        "github.com/apache/answer/internal/service/service_config"
        "github.com/apache/answer/pkg/writer"
@@ -98,7 +98,7 @@ func (c *AllConfig) SetEnvironmentOverrides() {
 // ReadConfig read config
 func ReadConfig(configFilePath string) (c *AllConfig, err error) {
        if len(configFilePath) == 0 {
-               configFilePath = filepath.Join(cli.ConfigFileDir, 
cli.DefaultConfigFileName)
+               configFilePath = filepath.Join(path.ConfigFileDir, 
path.DefaultConfigFileName)
        }
        c = &AllConfig{}
        config, err := viper.NewWithPath(configFilePath)
diff --git a/internal/base/path/path.go b/internal/base/path/path.go
new file mode 100644
index 00000000..bcd3665b
--- /dev/null
+++ b/internal/base/path/path.go
@@ -0,0 +1,53 @@
+/*
+ * 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 path
+
+import (
+       "path/filepath"
+       "sync"
+)
+
+const (
+       DefaultConfigFileName                  = "config.yaml"
+       DefaultCacheFileName                   = "cache.db"
+       DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
+)
+
+var (
+       ConfigFileDir     = "/conf/"
+       UploadFilePath    = "/uploads/"
+       I18nPath          = "/i18n/"
+       CacheDir          = "/cache/"
+       formatAllPathOnce sync.Once
+)
+
+func FormatAllPath(dataDirPath string) {
+       formatAllPathOnce.Do(func() {
+               ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
+               UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
+               I18nPath = filepath.Join(dataDirPath, I18nPath)
+               CacheDir = filepath.Join(dataDirPath, CacheDir)
+       })
+}
+
+// GetConfigFilePath get config file path
+func GetConfigFilePath() string {
+       return filepath.Join(ConfigFileDir, DefaultConfigFileName)
+}
diff --git a/internal/cli/install.go b/internal/cli/install.go
index 69e673d5..65055580 100644
--- a/internal/cli/install.go
+++ b/internal/cli/install.go
@@ -23,45 +23,17 @@ import (
        "fmt"
        "os"
        "path/filepath"
-       "sync"
 
        "github.com/apache/answer/configs"
        "github.com/apache/answer/i18n"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/pkg/dir"
        "github.com/apache/answer/pkg/writer"
 )
 
-const (
-       DefaultConfigFileName                  = "config.yaml"
-       DefaultCacheFileName                   = "cache.db"
-       DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
-)
-
-var (
-       ConfigFileDir     = "/conf/"
-       UploadFilePath    = "/uploads/"
-       I18nPath          = "/i18n/"
-       CacheDir          = "/cache/"
-       formatAllPathONCE sync.Once
-)
-
-// GetConfigFilePath get config file path
-func GetConfigFilePath() string {
-       return filepath.Join(ConfigFileDir, DefaultConfigFileName)
-}
-
-func FormatAllPath(dataDirPath string) {
-       formatAllPathONCE.Do(func() {
-               ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
-               UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
-               I18nPath = filepath.Join(dataDirPath, I18nPath)
-               CacheDir = filepath.Join(dataDirPath, CacheDir)
-       })
-}
-
 // InstallAllInitialEnvironment install all initial environment
 func InstallAllInitialEnvironment(dataDirPath string) {
-       FormatAllPath(dataDirPath)
+       path.FormatAllPath(dataDirPath)
        installUploadDir()
        InstallI18nBundle(false)
        fmt.Println("install all initial environment done")
@@ -69,7 +41,7 @@ func InstallAllInitialEnvironment(dataDirPath string) {
 
 func InstallConfigFile(configFilePath string) error {
        if len(configFilePath) == 0 {
-               configFilePath = filepath.Join(ConfigFileDir, 
DefaultConfigFileName)
+               configFilePath = filepath.Join(path.ConfigFileDir, 
path.DefaultConfigFileName)
        }
        fmt.Println("[config-file] try to create at ", configFilePath)
 
@@ -79,7 +51,7 @@ func InstallConfigFile(configFilePath string) error {
                return nil
        }
 
-       if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil {
+       if err := dir.CreateDirIfNotExist(path.ConfigFileDir); err != nil {
                fmt.Printf("[config-file] create directory fail %s\n", 
err.Error())
                return fmt.Errorf("create directory fail %s", err.Error())
        }
@@ -95,10 +67,10 @@ func InstallConfigFile(configFilePath string) error {
 
 func installUploadDir() {
        fmt.Println("[upload-dir] try to install...")
-       if err := dir.CreateDirIfNotExist(UploadFilePath); err != nil {
+       if err := dir.CreateDirIfNotExist(path.UploadFilePath); err != nil {
                fmt.Printf("[upload-dir] install fail %s\n", err.Error())
        } else {
-               fmt.Printf("[upload-dir] install success, upload directory is 
%s\n", UploadFilePath)
+               fmt.Printf("[upload-dir] install success, upload directory is 
%s\n", path.UploadFilePath)
        }
 }
 
@@ -108,7 +80,7 @@ func InstallI18nBundle(replace bool) {
        if len(os.Getenv("SKIP_REPLACE_I18N")) > 0 {
                replace = false
        }
-       if err := dir.CreateDirIfNotExist(I18nPath); err != nil {
+       if err := dir.CreateDirIfNotExist(path.I18nPath); err != nil {
                fmt.Println(err.Error())
                return
        }
@@ -120,7 +92,7 @@ func InstallI18nBundle(replace bool) {
        }
        fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList))
        for _, item := range i18nList {
-               path := filepath.Join(I18nPath, item.Name())
+               path := filepath.Join(path.I18nPath, item.Name())
                content, err := i18n.I18n.ReadFile(item.Name())
                if err != nil {
                        continue
diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go
index 75ed5d3b..9326e069 100644
--- a/internal/cli/install_check.go
+++ b/internal/cli/install_check.go
@@ -23,6 +23,7 @@ import (
        "fmt"
 
        "github.com/apache/answer/internal/base/data"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/entity"
        "github.com/apache/answer/pkg/dir"
 )
@@ -32,7 +33,7 @@ func CheckConfigFile(configPath string) bool {
 }
 
 func CheckUploadDir() bool {
-       return dir.CheckDirExist(UploadFilePath)
+       return dir.CheckDirExist(path.UploadFilePath)
 }
 
 // CheckDBConnection check database whether the connection is normal
diff --git a/internal/cli/reset_password.go b/internal/cli/reset_password.go
new file mode 100644
index 00000000..2a7d1af4
--- /dev/null
+++ b/internal/cli/reset_password.go
@@ -0,0 +1,288 @@
+/*
+ * 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 cli
+
+import (
+       "bufio"
+       "context"
+       "crypto/rand"
+       "fmt"
+       "math/big"
+       "os"
+       "runtime"
+       "strings"
+
+       "github.com/apache/answer/internal/base/conf"
+       "github.com/apache/answer/internal/base/data"
+       "github.com/apache/answer/internal/base/path"
+       "github.com/apache/answer/internal/repo/auth"
+       "github.com/apache/answer/internal/repo/user"
+       authService "github.com/apache/answer/internal/service/auth"
+       "github.com/apache/answer/pkg/checker"
+       _ "github.com/go-sql-driver/mysql"
+       _ "github.com/lib/pq"
+       "golang.org/x/crypto/bcrypt"
+       "golang.org/x/term"
+       _ "modernc.org/sqlite"
+       "xorm.io/xorm"
+)
+
+const (
+       charsetLower                = "abcdefghijklmnopqrstuvwxyz"
+       charsetUpper                = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+       charsetDigits               = "0123456789"
+       charsetSpecial              = "!@#$%^&*~?_-"
+       maxRetries                  = 10
+       defaultRandomPasswordLength = 12
+)
+
+var charset = []string{
+       charsetLower,
+       charsetUpper,
+       charsetDigits,
+       charsetSpecial,
+}
+
+type ResetPasswordOptions struct {
+       Email    string
+       Password string
+}
+
+func ResetPassword(ctx context.Context, dataDirPath string, opts 
*ResetPasswordOptions) error {
+       path.FormatAllPath(dataDirPath)
+
+       config, err := conf.ReadConfig(path.GetConfigFilePath())
+       if err != nil {
+               return fmt.Errorf("read config file failed: %w", err)
+       }
+
+       db, err := initDatabase(config.Data.Database.Driver, 
config.Data.Database.Connection)
+       if err != nil {
+               return fmt.Errorf("connect database failed: %w", err)
+       }
+       defer db.Close()
+
+       cache, cacheCleanup, err := data.NewCache(config.Data.Cache)
+       if err != nil {
+               return fmt.Errorf("initialize cache failed: %w", err)
+       }
+       defer cacheCleanup()
+
+       dataData, dataCleanup, err := data.NewData(db, cache)
+       if err != nil {
+               return fmt.Errorf("initialize data layer failed: %w", err)
+       }
+       defer dataCleanup()
+
+       userRepo := user.NewUserRepo(dataData)
+       authRepo := auth.NewAuthRepo(dataData)
+       authSvc := authService.NewAuthService(authRepo)
+
+       email := strings.TrimSpace(opts.Email)
+       if email == "" {
+               reader := bufio.NewReader(os.Stdin)
+               fmt.Print("Please input user email: ")
+               emailInput, err := reader.ReadString('\n')
+               if err != nil {
+                       return fmt.Errorf("read email input failed: %w", err)
+               }
+               email = strings.TrimSpace(emailInput)
+       }
+
+       userInfo, exist, err := userRepo.GetByEmail(ctx, email)
+       if err != nil {
+               return fmt.Errorf("query user failed: %w", err)
+       }
+       if !exist {
+               return fmt.Errorf("user not found: %s", email)
+       }
+
+       fmt.Printf("You are going to reset password for user: %s\n", email)
+
+       password := strings.TrimSpace(opts.Password)
+
+       if password != "" {
+               printWarning("Passing password via command line may be recorded 
in shell history")
+               if err := checker.CheckPassword(password); err != nil {
+                       return fmt.Errorf("password validation failed: %w", err)
+               }
+       } else {
+               password, err = promptForPassword()
+               if err != nil {
+                       return fmt.Errorf("password input failed: %w", err)
+               }
+       }
+
+       if !confirmAction(fmt.Sprintf("This will reset password for user 
'[%s]%s'. Continue?", userInfo.DisplayName, email)) {
+               fmt.Println("Operation cancelled")
+               return nil
+       }
+
+       hashPwd, err := bcrypt.GenerateFromPassword([]byte(password), 
bcrypt.DefaultCost)
+       if err != nil {
+               return fmt.Errorf("encrypt password failed: %w", err)
+       }
+
+       if err = userRepo.UpdatePass(ctx, userInfo.ID, string(hashPwd)); err != 
nil {
+               return fmt.Errorf("update password failed: %w", err)
+       }
+
+       authSvc.RemoveUserAllTokens(ctx, userInfo.ID)
+
+       fmt.Printf("Password has been successfully updated for user: %s\n", 
email)
+       fmt.Println("All login sessions have been cleared")
+
+       return nil
+}
+
+// promptForPassword prompts for a password
+func promptForPassword() (string, error) {
+       for {
+               input, err := getPasswordInput("Please input new password 
(empty to generate random password): ")
+               if err != nil {
+                       return "", err
+               }
+
+               if input == "" {
+                       password, err := generateRandomPasswordWithRetry()
+                       if err != nil {
+                               return "", fmt.Errorf("generate random password 
failed: %w", err)
+                       }
+                       fmt.Printf("Generated random password: %s\n", password)
+                       fmt.Println("Please save this password in a secure 
location")
+                       return password, nil
+               }
+
+               if err := checker.CheckPassword(input); err != nil {
+                       fmt.Printf("Password validation failed: %v\n", err)
+                       fmt.Println("Please try again")
+                       continue
+               }
+
+               confirmPwd, err := getPasswordInput("Please confirm new 
password: ")
+               if err != nil {
+                       return "", err
+               }
+
+               if input != confirmPwd {
+                       fmt.Println("Passwords do not match, please try again")
+                       continue
+               }
+
+               return input, nil
+       }
+}
+
+func generateRandomPasswordWithRetry() (string, error) {
+       var password string
+       var err error
+
+       for range maxRetries {
+               password, err = 
generateRandomPassword(defaultRandomPasswordLength)
+               if err != nil {
+                       continue
+               }
+               if err := checker.CheckPassword(password); err == nil {
+                       return password, nil
+               }
+       }
+
+       if err != nil {
+               return "", err
+       }
+       return "", fmt.Errorf("failed to generate valid password after %d 
retries", maxRetries)
+}
+
+func getPasswordInput(prompt string) (string, error) {
+       fmt.Print(prompt)
+       password, err := term.ReadPassword(int(os.Stdin.Fd()))
+       if err != nil {
+               return "", err
+       }
+       fmt.Println()
+       return string(password), nil
+}
+
+func generateRandomPassword(length int) (string, error) {
+       if length < len(charset) {
+               return "", fmt.Errorf("password length must be at least %d", 
len(charset))
+       }
+
+       bytes := make([]byte, length)
+       for i, charsetItem := range charset {
+               charIndex, err := rand.Int(rand.Reader, 
big.NewInt(int64(len(charsetItem))))
+               if err != nil {
+                       return "", err
+               }
+               bytes[i] = charsetItem[charIndex.Int64()]
+       }
+
+       fullCharset := strings.Join(charset, "")
+       for i := len(charset); i < length; i++ {
+               charIndex, err := rand.Int(rand.Reader, 
big.NewInt(int64(len(fullCharset))))
+               if err != nil {
+                       return "", err
+               }
+               bytes[i] = fullCharset[charIndex.Int64()]
+       }
+
+       for i := len(bytes) - 1; i > 0; i-- {
+               j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
+               if err != nil {
+                       return "", err
+               }
+               bytes[i], bytes[j.Int64()] = bytes[j.Int64()], bytes[i]
+       }
+
+       return string(bytes), nil
+}
+
+func initDatabase(driver, connection string) (*xorm.Engine, error) {
+       dataConf := &data.Database{Driver: driver, Connection: connection}
+       if !CheckDBConnection(dataConf) {
+               return nil, fmt.Errorf("database connection check failed")
+       }
+
+       engine, err := data.NewDB(false, dataConf)
+       if err != nil {
+               return nil, err
+       }
+
+       return engine, nil
+}
+
+func printWarning(msg string) {
+       if runtime.GOOS == "windows" {
+               fmt.Printf("[WARNING] %s\n", msg)
+       } else {
+               fmt.Printf("\033[31m[WARNING] %s\033[0m\n", msg)
+       }
+}
+
+func confirmAction(prompt string) bool {
+       reader := bufio.NewReader(os.Stdin)
+       fmt.Printf("%s [y/N]: ", prompt)
+       response, err := reader.ReadString('\n')
+       if err != nil {
+               return false
+       }
+       response = strings.ToLower(strings.TrimSpace(response))
+       return response == "y" || response == "yes"
+}
diff --git a/internal/install/install_controller.go 
b/internal/install/install_controller.go
index d43bbb45..08b4c978 100644
--- a/internal/install/install_controller.go
+++ b/internal/install/install_controller.go
@@ -30,6 +30,7 @@ import (
        "github.com/apache/answer/internal/base/conf"
        "github.com/apache/answer/internal/base/data"
        "github.com/apache/answer/internal/base/handler"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/base/reason"
        "github.com/apache/answer/internal/base/translator"
        "github.com/apache/answer/internal/cli"
@@ -62,7 +63,7 @@ func LangOptions(ctx *gin.Context) {
 // @Success 200 {object} handler.RespBody{}
 // @Router /installation/language/config [get]
 func GetLangMapping(ctx *gin.Context) {
-       t, err := translator.NewTranslator(&translator.I18n{BundleDir: 
cli.I18nPath})
+       t, err := translator.NewTranslator(&translator.I18n{BundleDir: 
path.I18nPath})
        if err != nil {
                handler.HandleResponse(ctx, err, nil)
                return
@@ -186,9 +187,9 @@ func InitEnvironment(ctx *gin.Context) {
        }
        c.Data.Database.Driver = req.DbType
        c.Data.Database.Connection = req.GetConnection()
-       c.Data.Cache.FilePath = filepath.Join(cli.CacheDir, 
cli.DefaultCacheFileName)
-       c.I18n.BundleDir = cli.I18nPath
-       c.ServiceConfig.UploadPath = cli.UploadFilePath
+       c.Data.Cache.FilePath = filepath.Join(path.CacheDir, 
path.DefaultCacheFileName)
+       c.I18n.BundleDir = path.I18nPath
+       c.ServiceConfig.UploadPath = path.UploadFilePath
 
        if err := conf.RewriteConfig(confPath, c); err != nil {
                log.Errorf("rewrite config failed %s", err)
diff --git a/internal/install/install_main.go b/internal/install/install_main.go
index 0d65f33f..41ccdcf4 100644
--- a/internal/install/install_main.go
+++ b/internal/install/install_main.go
@@ -23,8 +23,8 @@ import (
        "fmt"
        "os"
 
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/internal/base/translator"
-       "github.com/apache/answer/internal/cli"
 )
 
 var (
@@ -35,7 +35,7 @@ var (
 func Run(configPath string) {
        confPath = configPath
        // initialize translator for return internationalization error when 
installing.
-       _, err := translator.NewTranslator(&translator.I18n{BundleDir: 
cli.I18nPath})
+       _, err := translator.NewTranslator(&translator.I18n{BundleDir: 
path.I18nPath})
        if err != nil {
                panic(err)
        }
diff --git a/pkg/checker/reserved_username.go b/pkg/checker/reserved_username.go
index 6dfd3471..0971b527 100644
--- a/pkg/checker/reserved_username.go
+++ b/pkg/checker/reserved_username.go
@@ -26,7 +26,7 @@ import (
        "sync"
 
        "github.com/apache/answer/configs"
-       "github.com/apache/answer/internal/cli"
+       "github.com/apache/answer/internal/base/path"
        "github.com/apache/answer/pkg/dir"
 )
 
@@ -36,7 +36,7 @@ var (
 )
 
 func initReservedUsername() {
-       reservedUsernamesJsonFilePath := filepath.Join(cli.ConfigFileDir, 
cli.DefaultReservedUsernamesConfigFileName)
+       reservedUsernamesJsonFilePath := filepath.Join(path.ConfigFileDir, 
path.DefaultReservedUsernamesConfigFileName)
        if dir.CheckFileExist(reservedUsernamesJsonFilePath) {
                // if reserved username file exists, read it and replace 
configuration
                reservedUsernamesJsonFile, err := 
os.ReadFile(reservedUsernamesJsonFilePath)

Reply via email to