package main import ( "bufio" "context" "encoding/json" "flag" "fmt" "log" "net/http" "os" "time" "github.com/autobrr/autobrr/internal/config" "github.com/autobrr/autobrr/internal/database" "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/logger" "github.com/autobrr/autobrr/pkg/argon2id" "github.com/autobrr/autobrr/pkg/errors" "golang.org/x/term" _ "modernc.org/sqlite" ) const usage = `usage: autobrrctl --config path create-user Create user change-password Change password for user version Can be run without --config help Show this help message ` var ( version = "dev" commit = "" date = "" owner = "autobrr" repo = "autobrr" ) func init() { flag.Usage = func() { fmt.Fprint(flag.CommandLine.Output(), usage) } } func main() { var configPath string flag.StringVar(&configPath, "config", "", "path to configuration file") flag.Parse() switch cmd := flag.Arg(0); cmd { case "version": fmt.Printf("Version: %v\nCommit: %v\nBuild: %v\n", version, commit, date) // get the latest release tag from brr-api client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(fmt.Sprintf("https://api.autobrr.com/repos/%s/%s/releases/latest", owner, repo)) if err != nil { if errors.Is(err, http.ErrHandlerTimeout) { fmt.Println("Server timed out while fetching latest release from api") } else { fmt.Printf("Failed to fetch latest release from api: %v\n", err) } os.Exit(1) } defer resp.Body.Close() // brr-api returns 500 instead of 404 here if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError { fmt.Printf("No release found for %s/%s\n", owner, repo) os.Exit(1) } var rel struct { TagName string `json:"tag_name"` } if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil { fmt.Printf("Failed to decode response from api: %v\n", err) os.Exit(1) } fmt.Printf("Latest release: %v\n", rel.TagName) case "create-user": if configPath == "" { log.Fatal("--config required") } // read config cfg := config.New(configPath, version) // init new logger l := logger.New(cfg.Config) // open database connection db, _ := database.NewDB(cfg.Config, l) if err := db.Open(); err != nil { log.Fatal("could not open db connection") } userRepo := database.NewUserRepo(l, db) username := flag.Arg(1) if username == "" { flag.Usage() os.Exit(1) } password, err := readPassword() if err != nil { log.Fatalf("failed to read password: %v", err) } hashed, err := argon2id.CreateHash(string(password), argon2id.DefaultParams) if err != nil { log.Fatalf("failed to hash password: %v", err) } user := domain.CreateUserRequest{ Username: username, Password: hashed, } if err := userRepo.Store(context.Background(), user); err != nil { log.Fatalf("failed to create user: %v", err) } case "change-password": if configPath == "" { log.Fatal("--config required") } // read config cfg := config.New(configPath, version) // init new logger l := logger.New(cfg.Config) // open database connection db, _ := database.NewDB(cfg.Config, l) if err := db.Open(); err != nil { log.Fatal("could not open db connection") } userRepo := database.NewUserRepo(l, db) username := flag.Arg(1) if username == "" { flag.Usage() os.Exit(1) } user, err := userRepo.FindByUsername(context.Background(), username) if err != nil { log.Fatalf("failed to get user: %v", err) } if user == nil { log.Fatalf("failed to get user: %v", err) } password, err := readPassword() if err != nil { log.Fatalf("failed to read password: %v", err) } hashed, err := argon2id.CreateHash(string(password), argon2id.DefaultParams) if err != nil { log.Fatalf("failed to hash password: %v", err) } user.Password = hashed if err := userRepo.Update(context.Background(), *user); err != nil { log.Fatalf("failed to create user: %v", err) } default: flag.Usage() if cmd != "help" { os.Exit(1) } } } func readPassword() ([]byte, error) { var password []byte var err error fd := int(os.Stdin.Fd()) if term.IsTerminal(fd) { fmt.Printf("Password: ") password, err = term.ReadPassword(int(os.Stdin.Fd())) if err != nil { return nil, err } fmt.Printf("\n") } else { //fmt.Fprintf(os.Stderr, "warning: Reading password from stdin.\n") scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() { if err := scanner.Err(); err != nil { log.Fatalf("failed to read password from stdin: %v", err) } log.Fatalf("failed to read password from stdin: stdin is empty %v", err) } password = scanner.Bytes() if len(password) == 0 { return nil, errors.New("zero length password") } } return password, nil }