Files
panel/controller/cmd/reset-admin/main.go
T
2026-07-14 23:18:05 -07:00

117 lines
3.0 KiB
Go

// 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 <password>
// 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 <password> (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)
}