v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-19 18:15:31 -04:00
1 parent a999177f0c
commit e1fa5242e3
48 files changed
+2101 -135

No files matched your search

+47 -1
View File
@@ -5,6 +5,7 @@ import (
"errors"
"flag"
"fmt"
"net/url"
"os"
"strings"
"syscall"
@@ -12,6 +13,7 @@ import (
"time"
"github.com/lixenwraith/chess/internal/server/replay"
"github.com/lixenwraith/chess/internal/server/service"
"github.com/lixenwraith/chess/internal/server/storage"
"github.com/google/uuid"
@@ -40,7 +42,7 @@ func Run(args []string) error {
return runVerify(args[1:])
case "user":
if len(args) < 2 {
return fmt.Errorf("user subcommand required: add, delete, set-password, set-hash, set-email, set-username, list")
return fmt.Errorf("user subcommand required: add, delete, set-password, set-hash, set-email, set-username, reset-link, list")
}
return runUser(args[1], args[2:])
default:
@@ -303,6 +305,8 @@ func runUser(subcommand string, args []string) error {
return runUserSetEmail(args)
case "set-username":
return runUserSetUsername(args)
case "reset-link":
return runUserResetLink(args)
case "list":
return runUserList(args)
default:
@@ -310,6 +314,48 @@ func runUser(subcommand string, args []string) error {
}
}
// runUserResetLink prints a one-time password-reset link for an account, the
// same link the recovery email carries, for an administrator to pass on when
// the server sends no mail or the account has no email. It works for an hour
// and replaces any pending link.
func runUserResetLink(args []string) error {
fs, dsn := newFlagSet("user reset-link")
username := fs.String("username", "", "Username")
userID := fs.String("id", "", "User ID")
publicURL := fs.String("url", "", "Web client address the link opens, e.g. https://example.com/chess/web-client/ (required)")
if err := fs.Parse(args); err != nil {
return err
}
base, err := url.Parse(*publicURL)
if *publicURL == "" || err != nil || base.Scheme == "" || base.Host == "" {
return fmt.Errorf("-url must be the web client's absolute address")
}
store, err := openStore(*dsn, true)
if err != nil {
return err
}
defer store.Close()
targetID, err := lookupUser(store, *username, *userID)
if err != nil {
return err
}
token, hash, err := service.NewResetToken()
if err != nil {
return err
}
now := time.Now().UTC()
if err := store.CreatePasswordReset(storage.PasswordResetRecord{
UserID: targetID, TokenHash: hash, CreatedAt: now, ExpiresAt: now.Add(service.PasswordResetTTL),
}); err != nil {
return fmt.Errorf("failed to create reset: %w", err)
}
fmt.Printf("Reset link for %s, valid until %s (one use):\n%s\n",
abbreviateID(targetID), now.Add(service.PasswordResetTTL).Format(time.RFC3339), service.ResetLink(base, token))
return nil
}
// readPassword prompts on the terminal without echo.
func readPassword(prompt string) (string, error) {
fmt.Print(prompt)