From b39add4362db038fade4842006021cb9b6233477 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 20 Jun 2026 19:20:59 +0200 Subject: [PATCH] refactor: remove user command --- cmd/README.md | 14 --- cmd/user/main.go | 260 ----------------------------------------------- 2 files changed, 274 deletions(-) delete mode 100644 cmd/README.md delete mode 100644 cmd/user/main.go diff --git a/cmd/README.md b/cmd/README.md deleted file mode 100644 index 56ff93e..0000000 --- a/cmd/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# cmd - -Application entrypoints. - -| binary | purpose | -| ------------ | -------------------------------- | -| `cmd/server` | HTTP server and worker processes | -| `cmd/user` | User management CLI | - -## Conventions - -- Each subdirectory is a `package main` that compiles to a standalone binary. -- Shared logic lives in `internal/` or `pkg/`, not in `cmd/`. -- Configuration is read from environment variables — see each binary's `main.go` for the full list. diff --git a/cmd/user/main.go b/cmd/user/main.go deleted file mode 100644 index 306b738..0000000 --- a/cmd/user/main.go +++ /dev/null @@ -1,260 +0,0 @@ -// Package main provides small CLI utilities for local admin tasks. -package main - -import ( - "bufio" - "context" - "database/sql" - "errors" - "fmt" - "os" - "strings" - - "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" - "mal/internal" - "mal/internal/config" - "mal/internal/db" - "mal/internal/observability" - errlog "mal/pkg" -) - -func main() { - cfg, err := config.Load() - if err != nil { - observability.Error("cli_config_load_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - - dbConn, err := db.Open(cfg.DatabaseFile) - if err != nil { - observability.Error("cli_db_open_failed", "cmd/user", "", map[string]any{"db_file": cfg.DatabaseFile}, err) - os.Exit(1) - } - defer errlog.Close(dbConn, "failed to close cli database") - - os.Exit(run(dbConn, os.Args)) -} - -func run(dbConn *sql.DB, args []string) int { - ctx := context.Background() - - cmd, err := parseArgs(args) - if err != nil { - observability.Warn("cli_usage", "cmd/user", "invalid arguments", map[string]any{"argc": len(args)}, err) - if _, writeErr := fmt.Fprintln(os.Stderr, usage()); writeErr != nil { - observability.Warn("cli_usage_write_failed", "cmd/user", "", nil, writeErr) - } - return 2 - } - - switch cmd.kind { - case commandUpdateAvatar: - updateAvatars(ctx, dbConn) - return 0 - case commandRunFixes: - runFixes(ctx, dbConn) - return 0 - case commandCreateOrUpdateUser: - if err := createOrUpdateUser(ctx, dbConn, cmd.username, cmd.password); err != nil { - return 1 - } - return 0 - default: - observability.Error("cli_command_unreachable", "cmd/user", "", map[string]any{"kind": cmd.kind}, errors.New("unhandled command")) - return 1 - } -} - -type commandKind string - -const ( - commandUpdateAvatar commandKind = "update-avatar" - commandRunFixes commandKind = "run-fixes" - commandCreateOrUpdateUser commandKind = "create-or-update-user" -) - -type command struct { - kind commandKind - username string - password string -} - -func parseArgs(args []string) (command, error) { - if len(args) == 2 { - switch args[1] { - case string(commandUpdateAvatar): - return command{kind: commandUpdateAvatar}, nil - case string(commandRunFixes): - return command{kind: commandRunFixes}, nil - } - } - - if len(args) == 3 { - return command{ - kind: commandCreateOrUpdateUser, - username: args[1], - password: args[2], - }, nil - } - - return command{}, errors.New("invalid arguments") -} - -func usage() string { - return "Usage: go run cmd/user/main.go \n go run cmd/user/main.go update-avatar\n go run cmd/user/main.go run-fixes" -} - -func createOrUpdateUser(ctx context.Context, dbConn *sql.DB, username string, password string) error { - existingID, err := lookupUserID(ctx, dbConn, username) - if err != nil { - observability.Error("cli_user_lookup_failed", "cmd/user", "", map[string]any{"username": username}, err) - return err - } - - if existingID != "" { - if !promptConfirmOverwrite(username) { - fmt.Println("Operation cancelled.") - return nil - } - if err := updateUserPassword(ctx, dbConn, existingID, username, password); err != nil { - return err - } - fmt.Printf("Password for '%s' updated successfully!\n", username) - return nil - } - - if err := createUser(ctx, dbConn, username, password); err != nil { - return err - } - fmt.Printf("User '%s' was created successfully!\n", username) - return nil -} - -func lookupUserID(ctx context.Context, dbConn *sql.DB, username string) (string, error) { - var id string - err := dbConn.QueryRowContext(ctx, "SELECT id FROM user WHERE username = ?", username).Scan(&id) - if err == nil { - return id, nil - } - if errors.Is(err, sql.ErrNoRows) { - return "", nil - } - return "", err -} - -func promptConfirmOverwrite(username string) bool { - fmt.Printf("User '%s' already exists. Do you want to overwrite their password? [y/N]: ", username) - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') - if err != nil { - observability.Warn("cli_confirm_read_failed", "cmd/user", "", map[string]any{"username": username}, err) - return false - } - response = strings.TrimSpace(strings.ToLower(response)) - return response == "y" || response == "yes" -} - -func updateUserPassword(ctx context.Context, dbConn *sql.DB, userID string, username string, password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) - if err != nil { - observability.Error("cli_password_hash_failed", "cmd/user", "", nil, err) - return err - } - - _, err = dbConn.ExecContext(ctx, "UPDATE user SET password_hash = ? WHERE id = ?", string(hash), userID) - if err != nil { - observability.Error("cli_user_password_update_failed", "cmd/user", "", map[string]any{"username": username}, err) - return err - } - return nil -} - -func createUser(ctx context.Context, dbConn *sql.DB, username string, password string) error { - hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) - if err != nil { - observability.Error("cli_password_hash_failed", "cmd/user", "", nil, err) - return err - } - - id := uuid.New().String() - avatarURL := internal.DefaultAvatarURL(username) - _, err = dbConn.ExecContext( - ctx, - "INSERT INTO user (id, username, password_hash, avatar_url) VALUES (?, ?, ?, ?)", - id, - username, - string(hash), - avatarURL, - ) - if err != nil { - observability.Error("cli_user_create_failed", "cmd/user", "", map[string]any{"username": username}, err) - return err - } - return nil -} - -func updateAvatars(ctx context.Context, dbConn *sql.DB) { - rows, err := dbConn.QueryContext(ctx, "SELECT id, username FROM user") - if err != nil { - observability.Error("cli_users_list_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - defer errlog.Close(rows, "failed to close users rows") - - count := 0 - for rows.Next() { - var id, username string - if err := rows.Scan(&id, &username); err != nil { - observability.Error("cli_user_scan_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - - avatarURL := internal.DefaultAvatarURL(username) - _, err := dbConn.ExecContext(ctx, "UPDATE user SET avatar_url = ? WHERE id = ?", avatarURL, id) - if err != nil { - observability.Error("cli_user_avatar_update_failed", "cmd/user", "", map[string]any{"username": username}, err) - os.Exit(1) - } - count++ - } - - if err := rows.Err(); err != nil { - observability.Error("cli_users_iter_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - - fmt.Printf("Updated avatars for %d user(s)\n", count) -} - -func runFixes(ctx context.Context, dbConn *sql.DB) { - if err := internal.RunMigrationsAndFixes(dbConn); err != nil { - observability.Error("cli_run_migrations_and_fixes_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - - rows, err := dbConn.QueryContext(ctx, "SELECT id, applied_at FROM data_fixes ORDER BY id ASC") - if err != nil { - observability.Error("cli_data_fixes_list_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - defer errlog.Close(rows, "failed to close data fixes rows") - - count := 0 - for rows.Next() { - var id string - var appliedAt string - if err := rows.Scan(&id, &appliedAt); err != nil { - observability.Error("cli_data_fix_scan_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - fmt.Printf("%s applied_at=%s\n", id, appliedAt) - count++ - } - if err := rows.Err(); err != nil { - observability.Error("cli_data_fixes_iter_failed", "cmd/user", "", nil, err) - os.Exit(1) - } - - fmt.Printf("Applied fixes: %d\n", count) -}