package main import ( "bufio" "context" "errors" "flag" "fmt" "log/slog" "os" "strings" "golang.org/x/term" "botpontos/internal/app" "botpontos/internal/bootstrap" "botpontos/internal/config" "botpontos/internal/domain" ) const adminUsage = `uso: botpontos admin comandos: create-owner --username NOME cria a primeira conta owner do painel reset-password --username NOME redefine a senha de uma conta do painel bot-admin --user-id ID --enable|--disable liga ou desliga o admin do bot no Telegram set-credentials --user-id ID --email E grava e valida a credencial da PontoMais rotate-key recifra as credenciais com a chave atual As senhas são lidas do terminal, sem eco. Execute com: docker compose run --rm api admin ... ` func runAdmin(ctx context.Context, cfg config.Config, logger *slog.Logger, args []string) error { if len(args) == 0 { fmt.Fprint(os.Stderr, adminUsage) return errors.New("comando administrativo não informado") } built, err := bootstrap.Build(ctx, cfg, logger) if err != nil { return err } defer built.Pool.Close() svc := built.Service fs := flag.NewFlagSet("admin "+args[0], flag.ContinueOnError) username := fs.String("username", "", "nome da conta do painel") userID := fs.Int64("user-id", 0, "ID do usuário do Telegram (veja /usuarios)") email := fs.String("email", "", "e-mail de login na PontoMais") enable := fs.Bool("enable", false, "liga") disable := fs.Bool("disable", false, "desliga") if err := fs.Parse(args[1:]); err != nil { return err } switch args[0] { case "create-owner": if *username == "" { return errors.New("informe --username") } password, err := readNewPassword() if err != nil { return err } account, err := svc.CreateAccount(ctx, app.CLIActor, app.AccountInput{ Username: *username, Password: password, Role: domain.RoleOwner, }) if err != nil { return err } fmt.Printf("Conta owner %q criada (ID %d).\n", account.Username, account.ID) case "reset-password": if *username == "" { return errors.New("informe --username") } password, err := readNewPassword() if err != nil { return err } if err := svc.ResetPasswordByUsername(ctx, app.CLIActor, *username, password); err != nil { return err } fmt.Println("Senha redefinida. As sessões abertas dessa conta foram encerradas.") case "bot-admin": if *userID <= 0 || *enable == *disable { return errors.New("informe --user-id e exatamente um entre --enable e --disable") } if err := svc.SetBotAdmin(ctx, app.CLIActor, *userID, *enable); err != nil { return err } fmt.Println("Admin do bot atualizado.") case "set-credentials": if *userID <= 0 || *email == "" { return errors.New("informe --user-id e --email") } password, err := readPassword("Senha da PontoMais: ") if err != nil { return err } if err := svc.SetCredentials(ctx, app.CLIActor, *userID, *email, password); err != nil { return err } fmt.Println("Credencial validada na PontoMais e gravada cifrada.") case "rotate-key": count, err := svc.RotateKey(ctx) if err != nil { return err } fmt.Printf("%d credencial(is) recifrada(s) com a chave versão %d.\n", count, cfg.Crypto.KeyVersion) default: fmt.Fprint(os.Stderr, adminUsage) return fmt.Errorf("comando administrativo desconhecido: %s", args[0]) } return nil } func readNewPassword() (string, error) { first, err := readPassword("Nova senha (mínimo 12 caracteres): ") if err != nil { return "", err } second, err := readPassword("Repita a senha: ") if err != nil { return "", err } if first != second { return "", errors.New("as senhas não conferem") } return first, nil } // readPassword lê sem eco quando há terminal; senão lê uma linha da entrada padrão. func readPassword(prompt string) (string, error) { fmt.Fprint(os.Stderr, prompt) fd := int(os.Stdin.Fd()) if term.IsTerminal(fd) { raw, err := term.ReadPassword(fd) fmt.Fprintln(os.Stderr) return string(raw), err } line, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil && line == "" { return "", err } return strings.TrimRight(line, "\r\n"), nil }