tracker: add -changepw to change a user passphrase

Mirrors -adduser: prompts for a new passphrase (min 8 chars) or reads
TRACKER_PASSWORD, hashes with bcrypt, and updates the stored hash.
Returns an error if the user does not exist.
This commit is contained in:
2026-09-23 07:40:14 -07:00
parent 955de9d2f1
commit bbedaa548e
3 changed files with 76 additions and 0 deletions
+2
View File
@@ -23,6 +23,8 @@ go build -o tracker .
```
Non-interactive variant: `TRACKER_PASSWORD=... ./tracker -adduser kyle`.
Change an existing user's passphrase the same way:
`./tracker -changepw kyle` (or `TRACKER_PASSWORD=... ./tracker -changepw kyle`).
Defaults: listens on `:8420` (all interfaces, so it's reachable over Tailscale),
database at `~/.local/share/tracker/tracker.db`.
+8
View File
@@ -46,6 +46,14 @@ func (s *Store) UserByName(username string) (User, error) {
return u, err
}
func (s *Store) SetPassword(username, passwordHash string) error {
res, err := s.db.Exec(`UPDATE users SET password_hash = ? WHERE username = ?`, passwordHash, username)
if err != nil {
return err
}
return checkAffected(res)
}
// --- api keys ---
func (s *Store) CreateAPIKey(name, keyHash string) (int64, error) {
+66
View File
@@ -22,6 +22,7 @@ func main() {
dbPath := flag.String("db", defaultDB, "sqlite database path")
addr := flag.String("addr", ":8420", "listen address")
addUser := flag.String("adduser", "", "create a login user, then exit")
changepw := flag.String("changepw", "", "change a login user's passphrase, then exit")
flag.Parse()
if err := os.MkdirAll(filepath.Dir(*dbPath), 0o755); err != nil {
@@ -35,6 +36,13 @@ func main() {
return
}
if *changepw != "" {
if err := runChangePassword(*dbPath, *changepw); err != nil {
log.Fatal(err)
}
return
}
st, err := store.Open(*dbPath)
if err != nil {
log.Fatal(err)
@@ -106,3 +114,61 @@ func runAddUser(dbPath, username string) error {
fmt.Printf("user %q created. log in at /login\n", username)
return nil
}
func runChangePassword(dbPath, username string) error {
username = strings.TrimSpace(username)
if username == "" {
return errors.New("username cannot be empty")
}
if strings.ContainsAny(username, " \t") {
return errors.New("username cannot contain whitespace")
}
st, err := store.Open(dbPath)
if err != nil {
return err
}
defer st.Close()
if _, err := st.UserByName(username); err != nil {
if errors.Is(err, store.ErrNotFound) {
return fmt.Errorf("user %q not found", username)
}
return err
}
password := os.Getenv("TRACKER_PASSWORD")
if password == "" {
fmt.Print("new passphrase: ")
pw, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return err
}
fmt.Print("confirm: ")
confirmPw, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return err
}
password = string(pw)
if string(confirmPw) != password {
return errors.New("passphrases do not match")
}
}
if len(password) < 8 {
return errors.New("passphrase must be at least 8 characters")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
if err := st.SetPassword(username, string(hash)); err != nil {
return err
}
fmt.Printf("passphrase for %q updated\n", username)
return nil
}