feat(auth): enhance login flow with notifications and path normalization
- add success notification on successful login - show error notification with detailed message on login failure - normalize API paths to prevent double slashes and trailing slashes - redirect to login page only if not on login request or page
This commit is contained in:
87
cmd/cli/commands_password_hash.go
Normal file
87
cmd/cli/commands_password_hash.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
hashPassword string
|
||||
hashCost int
|
||||
hashFromStdin bool
|
||||
)
|
||||
|
||||
var passwordHashCmd = &cobra.Command{
|
||||
Use: "password-hash [password]",
|
||||
Short: "Generate a bcrypt password hash",
|
||||
Long: "Generate a bcrypt password hash for creating or updating users.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Annotations: map[string]string{
|
||||
"skip-config-load": "true",
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
checkError(runPasswordHash(args))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
passwordHashCmd.Flags().StringVarP(&hashPassword, "password", "p", "", "Password to hash")
|
||||
passwordHashCmd.Flags().IntVar(&hashCost, "cost", bcrypt.DefaultCost, "Bcrypt cost")
|
||||
passwordHashCmd.Flags().BoolVar(&hashFromStdin, "stdin", false, "Read password from stdin")
|
||||
}
|
||||
|
||||
func runPasswordHash(args []string) error {
|
||||
if hashCost < bcrypt.MinCost || hashCost > bcrypt.MaxCost {
|
||||
return fmt.Errorf("invalid cost %d (must be between %d and %d)", hashCost, bcrypt.MinCost, bcrypt.MaxCost)
|
||||
}
|
||||
|
||||
password, err := resolveHashPassword(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), hashCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(hashed))
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveHashPassword(args []string) (string, error) {
|
||||
if hashFromStdin && (hashPassword != "" || len(args) > 0) {
|
||||
return "", fmt.Errorf("use --stdin by itself (do not combine with --password or positional password)")
|
||||
}
|
||||
|
||||
if hashFromStdin {
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password from stdin: %w", err)
|
||||
}
|
||||
password := strings.TrimRight(string(data), "\r\n")
|
||||
if password == "" {
|
||||
return "", fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
if hashPassword != "" && len(args) > 0 {
|
||||
return "", fmt.Errorf("use either --password or positional password, not both")
|
||||
}
|
||||
|
||||
if hashPassword != "" {
|
||||
return hashPassword, nil
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
return args[0], nil
|
||||
}
|
||||
|
||||
return readPassword("Password: "), nil
|
||||
}
|
||||
@@ -126,7 +126,7 @@ var usersRemoveCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
usersCmd.PersistentFlags().StringVar(&serverConfigPath, "server-config", "", "server config file (default: config.json or ~/.whatshooked/config.json)")
|
||||
usersCmd.PersistentFlags().StringVar(&serverConfigPath, "server-config", "", "server config file (default: config.json, ../config.json, or ~/.whatshooked/config.json)")
|
||||
|
||||
usersAddCmd.Flags().StringVarP(&addUsername, "username", "u", "", "Username")
|
||||
usersAddCmd.Flags().StringVarP(&addEmail, "email", "e", "", "Email address")
|
||||
@@ -156,6 +156,9 @@ func resolveServerConfigPath() string {
|
||||
if _, err := os.Stat("config.json"); err == nil {
|
||||
return "config.json"
|
||||
}
|
||||
if _, err := os.Stat("../config.json"); err == nil {
|
||||
return "../config.json"
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
p := filepath.Join(home, ".whatshooked", "config.json")
|
||||
|
||||
@@ -25,6 +25,10 @@ var rootCmd = &cobra.Command{
|
||||
Short: "WhatsHooked CLI - Manage WhatsApp webhooks",
|
||||
Long: `A command-line interface for managing WhatsHooked server, hooks, and WhatsApp accounts.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if shouldSkipConfigLoad(cmd) {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cliConfig, err = LoadCLIConfig(cfgFile, serverURL)
|
||||
if err != nil {
|
||||
@@ -44,4 +48,14 @@ func init() {
|
||||
rootCmd.AddCommand(accountsCmd)
|
||||
rootCmd.AddCommand(sendCmd)
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
rootCmd.AddCommand(passwordHashCmd)
|
||||
}
|
||||
|
||||
func shouldSkipConfigLoad(cmd *cobra.Command) bool {
|
||||
for c := cmd; c != nil; c = c.Parent() {
|
||||
if c.Annotations != nil && c.Annotations["skip-config-load"] == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user