// reset-admin resets a panel user's password. // // Two modes: // // reset-admin --email admin@panel.local --password NEWPW [--db-url postgres://...] // Connects to Postgres (flag, or $PANEL_DB_URL, or the controller's // default dev DSN) and writes a fresh argon2id hash for that user. // // reset-admin // Legacy: prints the argon2id hash to stdout for a manual // `UPDATE users SET password_hash = '...'`. package main import ( "context" "crypto/rand" "encoding/base64" "flag" "fmt" "os" "time" "github.com/jackc/pgx/v5" "golang.org/x/crypto/argon2" ) // Must match controller/cmd/controller/auth.go hashPassword parameters. const ( argonTime = 3 argonMemory = 64 * 1024 argonThreads = 2 argonKeyLen = 32 argonSaltLen = 16 defaultDBURL = "postgres://panel:panel_dev@127.0.0.1:5432/panel?sslmode=disable" ) func hash(password string) (string, error) { salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", err } h := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(h), ), nil } func main() { email := flag.String("email", "", "user email to reset (writes the DB; e.g. admin@panel.local)") password := flag.String("password", "", "new password (used with --email)") dbURL := flag.String("db-url", "", "Postgres connection string (default: $PANEL_DB_URL, else the controller's dev default)") flag.Usage = func() { fmt.Fprintln(os.Stderr, `usage: reset-admin --email EMAIL --password NEWPW [--db-url postgres://...] reset-admin (hash-only: prints argon2id hash for manual UPDATE)`) } flag.Parse() // Legacy hash-only mode: one positional arg, no --email. if *email == "" { if flag.NArg() != 1 { flag.Usage() os.Exit(2) } h, err := hash(flag.Arg(0)) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(h) return } if *password == "" { fmt.Fprintln(os.Stderr, "reset-admin: --password is required with --email") os.Exit(2) } dsn := *dbURL if dsn == "" { dsn = os.Getenv("PANEL_DB_URL") } if dsn == "" { dsn = defaultDBURL } h, err := hash(*password) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() conn, err := pgx.Connect(ctx, dsn) if err != nil { fmt.Fprintf(os.Stderr, "reset-admin: connect: %v\n", err) os.Exit(1) } defer conn.Close(ctx) tag, err := conn.Exec(ctx, `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE email = $2`, h, *email) if err != nil { fmt.Fprintf(os.Stderr, "reset-admin: update: %v\n", err) os.Exit(1) } if tag.RowsAffected() == 0 { fmt.Fprintf(os.Stderr, "reset-admin: no user with email %q\n", *email) os.Exit(1) } fmt.Printf("reset-admin: password updated for %s\n", *email) }