auth.signUp never lets a caller pick a user id (users_id_seq always assigns it), but users.id is GENERATED BY DEFAULT rather than ALWAYS, so an explicit id in the INSERT is honored - the same mechanism ensureOfficialSystemUserWithDB already relies on to seed the built-in system accounts at fixed ids. createuser -id N [-phone ...|-email ...] inserts a user row at that id for local/dev use, refusing (unless -force) a reserved system-account id or one at/above UserIDSequenceBase where a future organic signup could collide with it. -email reproduces the real email-signup path exactly: a synthetic 888-prefixed display phone (domain.NewEmailSignupDisplayPhone, re-rolled on collision) plus the real address in signup_email, rather than storing the address in users.phone directly.
153 lines
5.6 KiB
Go
153 lines
5.6 KiB
Go
// Command createuser inserts a users row with an operator-chosen id, bypassing
|
|
// the normal users_id_seq auto-assignment. This works because users.id is
|
|
// GENERATED BY DEFAULT AS IDENTITY (not GENERATED ALWAYS) -- an explicit id in
|
|
// the INSERT is honored, the same mechanism ensureOfficialSystemUserWithDB
|
|
// (internal/store/postgres/message_send.go) already relies on to seed the
|
|
// built-in system accounts (ChatBot, BotFather, ...) at their fixed ids.
|
|
//
|
|
// Normal signup (auth.signUp) never lets a caller pick an id, so this exists
|
|
// purely for local/dev tooling -- reserving a specific low id (below
|
|
// OfficialSystemUserID=777000, say) for a test account.
|
|
//
|
|
// Usage:
|
|
//
|
|
// createuser -id 1000 [-first-name Test] [-last-name User] [-username testuser] -phone "15550001234"
|
|
// createuser -id 1000 [-first-name Test] [-last-name User] [-username testuser] -email "test@example.com"
|
|
//
|
|
// -phone and -email are mutually exclusive: an email-signup account never
|
|
// stores the address in users.phone directly (see internal/domain/emailphone.go)
|
|
// -- it gets a synthetic "888"-prefixed display phone instead (the same one
|
|
// assignEmailSignupDisplayPhone hands a real email-signup account), with the
|
|
// real address recorded separately in signup_email.
|
|
//
|
|
// Reads TELESRV_POSTGRES_DSN the same way the server does (internal/config).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"telesrv/internal/config"
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
// maxEmailSignupPhoneAttempts bounds the display-phone collision-retry loop,
|
|
// mirroring internal/app/auth/service.go's own constant of the same name.
|
|
const maxEmailSignupPhoneAttempts = 20
|
|
|
|
func randomInt64() (int64, error) {
|
|
var b [8]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return 0, fmt.Errorf("rand: %w", err)
|
|
}
|
|
return int64(binary.LittleEndian.Uint64(b[:])), nil
|
|
}
|
|
|
|
func main() {
|
|
id := flag.Int64("id", 0, "user id to create (required)")
|
|
firstName := flag.String("first-name", "Test", "first name")
|
|
lastName := flag.String("last-name", "", "last name")
|
|
username := flag.String("username", "", "username, without @ (optional)")
|
|
phone := flag.String("phone", "", "phone number (optional; mutually exclusive with -email)")
|
|
email := flag.String("email", "", "email address for an email-signup account (optional; mutually exclusive with -phone)")
|
|
force := flag.Bool("force", false, "skip the reserved-id / sequence-collision safety checks")
|
|
flag.Parse()
|
|
|
|
if *id <= 0 {
|
|
fmt.Fprintln(os.Stderr, "createuser: -id is required and must be positive")
|
|
os.Exit(2)
|
|
}
|
|
if *phone != "" && *email != "" {
|
|
fmt.Fprintln(os.Stderr, "createuser: -phone and -email are mutually exclusive")
|
|
os.Exit(2)
|
|
}
|
|
if !*force {
|
|
if domain.IsSystemUserID(*id) {
|
|
fmt.Fprintf(os.Stderr, "createuser: %d is a reserved built-in system account id (see internal/domain/system.go) - refusing, pass -force to override\n", *id)
|
|
os.Exit(2)
|
|
}
|
|
if *id >= domain.UserIDSequenceBase {
|
|
fmt.Fprintf(os.Stderr, "createuser: %d is >= UserIDSequenceBase (%d) - a future organic signup could eventually collide with it; pass -force to proceed anyway (then consider bumping users_id_seq yourself)\n", *id, domain.UserIDSequenceBase)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "createuser: load config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
pool, err := pgxpool.New(ctx, cfg.PostgresDSN)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "createuser: connect: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer pool.Close()
|
|
|
|
accessHash, err := randomInt64()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "createuser: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
displayPhone := *phone
|
|
signupEmail := ""
|
|
if *email != "" {
|
|
signupEmail = domain.NormalizeEmailForPhone(*email)
|
|
displayPhone, err = assignEmailSignupDisplayPhone(ctx, pool)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "createuser: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// phone/username/signup_email all sit under partial unique indexes that
|
|
// exclude '', so leaving any of them blank never collides with another
|
|
// blank-valued account.
|
|
row := pool.QueryRow(ctx, `
|
|
INSERT INTO users (id, access_hash, phone, signup_email, first_name, last_name, username, country_code)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, '')
|
|
ON CONFLICT (id) DO NOTHING
|
|
RETURNING id`,
|
|
*id, accessHash, displayPhone, signupEmail, *firstName, *lastName, *username)
|
|
|
|
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)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("created user id=%d access_hash=%d first_name=%q last_name=%q username=%q phone=%q signup_email=%q\n",
|
|
createdID, accessHash, *firstName, *lastName, *username, displayPhone, signupEmail)
|
|
}
|
|
|
|
// 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.
|
|
func assignEmailSignupDisplayPhone(ctx context.Context, pool *pgxpool.Pool) (string, error) {
|
|
for range maxEmailSignupPhoneAttempts {
|
|
candidate, err := domain.NewEmailSignupDisplayPhone(domain.EmailPhonePrefix)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var exists bool
|
|
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE phone = $1)`, candidate).Scan(&exists); err != nil {
|
|
return "", fmt.Errorf("check display phone collision: %w", err)
|
|
}
|
|
if !exists {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts)
|
|
}
|