Files
bot-pontos/backend/cmd/botpontos/admin.go
2026-10-23 22:31:02 -03:00

153 lines
4.2 KiB
Go

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 <comando>
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 <comando> ...
`
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
}