0a941f3ba6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
117 lines
3.0 KiB
Go
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)
|
|
}
|