// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package cmd import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "github.com/tamakyi/TamaBox/internal/conf" "github.com/tamakyi/TamaBox/internal/db" ) // Restore 恢复被自助停用的账号。 // // 自助停用(Deactivate)只清空内容、保留用户行作「身份占位」(软删除), // 因此恢复只需清掉 deleted_at;内容已不可找回。 // // 用法:TamaBox restore --email user@example.com // // TamaBox restore --domain someone --password 'new-password' var Restore = &cli.Command{ Name: "restore", Usage: "Restore a deactivated account (clears the identity placeholder)", Action: runRestore, Flags: []cli.Flag{ &cli.StringFlag{ Name: "email", Usage: "Email of the deactivated account", }, &cli.StringFlag{ Name: "domain", Usage: "Custom domain of the deactivated account", }, &cli.StringFlag{ Name: "password", Usage: "Optional: reset the password to this value (min 8 chars)", }, &cli.BoolFlag{ Name: "yes", Usage: "Skip confirmation", }, }, } func runRestore(ctx *cli.Context) error { if err := conf.Init(); err != nil { return errors.Wrap(err, "load configuration") } dbType := conf.Database.Type if dbType == "postgres" { conf.Database.DSN = conf.PostgresDsn() } else { conf.Database.DSN = conf.MySQLDsn() } database, err := db.Init(dbType, conf.Database.DSN) if err != nil { return errors.Wrap(err, "connect to database") } email := ctx.String("email") domain := ctx.String("domain") if email == "" && domain == "" { return errors.New("must provide --email or --domain") } newPassword := ctx.String("password") if newPassword != "" && len(newPassword) < 8 { return errors.New("password must be at least 8 characters") } // 用 Unscoped 查找:待恢复的账号必然是软删除状态,普通查询看不见 var user db.User query := database.WithContext(ctx.Context).Unscoped() if email != "" { query = query.Where("email = ?", email) } else { query = query.Where("domain = ?", domain) } if err := query.First(&user).Error; err != nil { return errors.Wrap(err, "find user to restore") } if !user.DeletedAt.Valid { return errors.Errorf("账号 %q 未被停用,无需恢复", user.Domain) } logrus.WithFields(logrus.Fields{ "id": user.ID, "name": user.Name, "email": user.Email, "domain": user.Domain, }).Warn("准备恢复以下已停用账号(提问/追问/私聊等内容在停用时已被删除,无法找回)") if !ctx.Bool("yes") { logrus.Warn("输入大写 YES 确认执行恢复:") var answer string if _, err := fmtScan(&answer); err != nil { return errors.Wrap(err, "read confirmation") } if answer != "YES" { return errors.New("已取消") } } if err := db.Users.Reactivate(ctx.Context, user.ID); err != nil { return errors.Wrap(err, "reactivate user") } // 密码重置必须在恢复之后:UpdatePassword 走普通作用域,软删除状态下找不到该用户 if newPassword != "" { if err := db.Users.UpdatePassword(ctx.Context, user.ID, newPassword); err != nil { return errors.Wrap(err, "reset password") } logrus.WithField("user_id", user.ID).Info("账号已恢复,密码已重置") return nil } logrus.WithField("user_id", user.ID).Info("账号已恢复(密码未改动,如原主已忘记密码请用 --password 重置)") return nil }