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)
