diff --git a/cmd/createuser/main.go b/cmd/createuser/main.go index 579a4e0c..d3557418 100644 --- a/cmd/createuser/main.go +++ b/cmd/createuser/main.go @@ -27,11 +27,14 @@ import ( "context" "crypto/rand" "encoding/binary" + "errors" "flag" "fmt" "os" "time" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "telesrv/internal/config" @@ -124,7 +127,7 @@ func main() { var createdID int64 if err := row.Scan(&createdID); err != nil { - fmt.Fprintf(os.Stderr, "createuser: id %d already exists (or insert failed): %v\n", *id, err) + fmt.Fprintln(os.Stderr, describeInsertFailure(*id, *username, displayPhone, signupEmail, err)) os.Exit(1) } @@ -132,6 +135,31 @@ func main() { createdID, accessHash, *firstName, *lastName, *username, displayPhone, signupEmail) } +// describeInsertFailure turns the INSERT's failure into a message naming the +// actual thing that collided, instead of "id already exists" for every case: +// ON CONFLICT (id) DO NOTHING only covers the id itself, so a duplicate +// username/phone/signup_email surfaces here as a distinct unique-violation +// error (pgx.ErrNoRows only means the id itself was the conflict). +func describeInsertFailure(id int64, username, phone, signupEmail string, err error) string { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + switch pgErr.ConstraintName { + case "users_username_lower_unique_idx": + return fmt.Sprintf("createuser: username %q is already taken", username) + case "users_phone_unique_idx": + return fmt.Sprintf("createuser: phone %q is already in use", phone) + case "users_signup_email_lower_unique_idx": + return fmt.Sprintf("createuser: email %q is already in use by another account", signupEmail) + default: + return fmt.Sprintf("createuser: unique constraint %q violated: %v", pgErr.ConstraintName, err) + } + } + if errors.Is(err, pgx.ErrNoRows) { + return fmt.Sprintf("createuser: id %d already exists", id) + } + return fmt.Sprintf("createuser: insert failed: %v", err) +} + // assignEmailSignupDisplayPhone mirrors internal/app/auth/service.go's method // of the same name: pick a random "888"-prefixed display phone and re-roll on // the astronomically unlikely collision with an existing account's phone.