feat(cli): enhance user and hook management with new commands and flags
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -22,6 +23,23 @@ import (
|
||||
|
||||
var serverConfigPath string
|
||||
|
||||
// flags for add
|
||||
var (
|
||||
addUsername string
|
||||
addEmail string
|
||||
addFullName string
|
||||
addPassword string
|
||||
addRole string
|
||||
)
|
||||
|
||||
// flags for update
|
||||
var (
|
||||
updateUsername string
|
||||
updateEmail string
|
||||
updateFullName string
|
||||
updateRole string
|
||||
)
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Manage users (direct DB access)",
|
||||
@@ -50,11 +68,15 @@ var usersAddCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var usersSetPasswordCmd = &cobra.Command{
|
||||
Use: "set-password <username>",
|
||||
Use: "set-password <username> [password]",
|
||||
Short: "Change a user's password",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
setPassword(args[0])
|
||||
if len(args) == 2 {
|
||||
setPasswordDirect(args[0], args[1])
|
||||
} else {
|
||||
setPassword(args[0])
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -105,6 +127,18 @@ var usersRemoveCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
usersCmd.PersistentFlags().StringVar(&serverConfigPath, "server-config", "", "server config file (default: config.json or ~/.whatshooked/config.json)")
|
||||
|
||||
usersAddCmd.Flags().StringVarP(&addUsername, "username", "u", "", "Username")
|
||||
usersAddCmd.Flags().StringVarP(&addEmail, "email", "e", "", "Email address")
|
||||
usersAddCmd.Flags().StringVarP(&addFullName, "full-name", "n", "", "Full name")
|
||||
usersAddCmd.Flags().StringVarP(&addPassword, "password", "p", "", "Password")
|
||||
usersAddCmd.Flags().StringVarP(&addRole, "role", "r", "", "Role (admin/user)")
|
||||
|
||||
usersUpdateCmd.Flags().StringVarP(&updateUsername, "username", "u", "", "New username")
|
||||
usersUpdateCmd.Flags().StringVarP(&updateEmail, "email", "e", "", "New email address")
|
||||
usersUpdateCmd.Flags().StringVarP(&updateFullName, "full-name", "n", "", "New full name")
|
||||
usersUpdateCmd.Flags().StringVarP(&updateRole, "role", "r", "", "New role (admin/user)")
|
||||
|
||||
usersCmd.AddCommand(usersListCmd)
|
||||
usersCmd.AddCommand(usersAddCmd)
|
||||
usersCmd.AddCommand(usersUpdateCmd)
|
||||
@@ -132,6 +166,41 @@ func resolveServerConfigPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// tryInitDB attempts to initialize the DB, returning false if it cannot.
|
||||
func tryInitDB() bool {
|
||||
if storage.DB != nil {
|
||||
return true
|
||||
}
|
||||
cfgPath := resolveServerConfigPath()
|
||||
if cfgPath == "" {
|
||||
return false
|
||||
}
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return storage.Initialize(&cfg.Database) == nil
|
||||
}
|
||||
|
||||
// dbOwnerUserID returns the ID of the first admin user (or first user) for use
|
||||
// when creating records via DB that require a user_id.
|
||||
func dbOwnerUserID(username string) string {
|
||||
var user models.ModelPublicUsers
|
||||
q := storage.DB.NewSelect().Model(&user)
|
||||
if username != "" {
|
||||
q = q.Where("username = ?", username)
|
||||
} else {
|
||||
q = q.Where("role = ?", "admin")
|
||||
}
|
||||
if err := q.Limit(1).Scan(context.Background()); err != nil {
|
||||
// fall back to any user
|
||||
if err2 := storage.DB.NewSelect().Model(&user).Limit(1).Scan(context.Background()); err2 != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return user.ID.String()
|
||||
}
|
||||
|
||||
func initUserDB() {
|
||||
cfgPath := resolveServerConfigPath()
|
||||
if cfgPath == "" {
|
||||
@@ -183,39 +252,29 @@ func listUsers() {
|
||||
}
|
||||
|
||||
func addUser() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
fmt.Print("Username: ")
|
||||
scanner.Scan()
|
||||
username := strings.TrimSpace(scanner.Text())
|
||||
username := addUsername
|
||||
if username == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: username is required")
|
||||
os.Exit(1)
|
||||
username = promptRequired("Username")
|
||||
}
|
||||
|
||||
fmt.Print("Email: ")
|
||||
scanner.Scan()
|
||||
email := strings.TrimSpace(scanner.Text())
|
||||
email := addEmail
|
||||
if email == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: email is required")
|
||||
os.Exit(1)
|
||||
email = promptRequired("Email")
|
||||
}
|
||||
|
||||
fmt.Print("Full Name (optional): ")
|
||||
scanner.Scan()
|
||||
fullName := strings.TrimSpace(scanner.Text())
|
||||
fullName := addFullName
|
||||
if fullName == "" {
|
||||
fullName = promptLine("Full Name (optional)", "")
|
||||
}
|
||||
|
||||
fmt.Print("Role (admin/user) [user]: ")
|
||||
scanner.Scan()
|
||||
role := strings.TrimSpace(scanner.Text())
|
||||
role := addRole
|
||||
if role == "" {
|
||||
role = "user"
|
||||
role = promptLine("Role (admin/user)", "user")
|
||||
}
|
||||
|
||||
password := readPassword("Password: ")
|
||||
password := addPassword
|
||||
if password == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: password is required")
|
||||
os.Exit(1)
|
||||
password = readPassword("Password: ")
|
||||
}
|
||||
|
||||
hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
@@ -240,8 +299,21 @@ func addUser() {
|
||||
fmt.Printf("User '%s' created\n", username)
|
||||
}
|
||||
|
||||
func setPassword(username string) {
|
||||
func setPasswordDirect(username, password string) {
|
||||
user := findUserByUsername(username)
|
||||
hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
checkError(err)
|
||||
_, err = storage.DB.NewUpdate().Model(user).
|
||||
Set("password = ?", string(hashedPw)).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", user.ID.String()).
|
||||
Exec(context.Background())
|
||||
checkError(err)
|
||||
fmt.Printf("Password updated for '%s'\n", username)
|
||||
}
|
||||
|
||||
func setPassword(username string) {
|
||||
findUserByUsername(username) // validate exists
|
||||
|
||||
password := readPassword("New password: ")
|
||||
confirm := readPassword("Confirm password: ")
|
||||
@@ -249,15 +321,15 @@ func setPassword(username string) {
|
||||
fmt.Fprintln(os.Stderr, "Error: passwords do not match")
|
||||
os.Exit(1)
|
||||
}
|
||||
if password == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: password cannot be empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
checkError(err)
|
||||
|
||||
_, err = storage.DB.NewUpdate().Model(user).
|
||||
var user models.ModelPublicUsers
|
||||
err = storage.DB.NewSelect().Model(&user).Where("username = ?", username).Scan(context.Background())
|
||||
checkError(err)
|
||||
|
||||
_, err = storage.DB.NewUpdate().Model(&user).
|
||||
Set("password = ?", string(hashedPw)).
|
||||
Set("updated_at = ?", time.Now()).
|
||||
Where("id = ?", user.ID.String()).
|
||||
@@ -286,36 +358,40 @@ func setUserActive(username string, active bool) {
|
||||
|
||||
func updateUser(username string) {
|
||||
user := findUserByUsername(username)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
fmt.Printf("Updating user '%s' (press Enter to keep current value)\n", username)
|
||||
// If any flag was provided, apply flags only (non-interactive)
|
||||
flagsProvided := updateUsername != "" || updateEmail != "" || updateFullName != "" || updateRole != ""
|
||||
|
||||
fmt.Printf("Username [%s]: ", user.Username.String())
|
||||
scanner.Scan()
|
||||
if v := strings.TrimSpace(scanner.Text()); v != "" {
|
||||
user.Username = resolvespec_common.NewSqlString(v)
|
||||
if flagsProvided {
|
||||
if updateUsername != "" {
|
||||
user.Username = resolvespec_common.NewSqlString(updateUsername)
|
||||
}
|
||||
if updateEmail != "" {
|
||||
user.Email = resolvespec_common.NewSqlString(updateEmail)
|
||||
}
|
||||
if updateFullName != "" {
|
||||
user.FullName = resolvespec_common.NewSqlString(updateFullName)
|
||||
}
|
||||
if updateRole != "" {
|
||||
user.Role = resolvespec_common.NewSqlString(updateRole)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Updating user '%s' (press Enter to keep current value)\n", username)
|
||||
|
||||
if v := promptLine("Username", user.Username.String()); v != "" {
|
||||
user.Username = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
if v := promptLine("Email", user.Email.String()); v != "" {
|
||||
user.Email = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
if v := promptLine("Full Name", user.FullName.String()); v != "" {
|
||||
user.FullName = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
if v := promptLine("Role (admin/user)", user.Role.String()); v != "" {
|
||||
user.Role = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Email [%s]: ", user.Email.String())
|
||||
scanner.Scan()
|
||||
if v := strings.TrimSpace(scanner.Text()); v != "" {
|
||||
user.Email = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
|
||||
fmt.Printf("Full Name [%s]: ", user.FullName.String())
|
||||
scanner.Scan()
|
||||
if v := strings.TrimSpace(scanner.Text()); v != "" {
|
||||
user.FullName = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
|
||||
fmt.Printf("Role (admin/user) [%s]: ", user.Role.String())
|
||||
scanner.Scan()
|
||||
if v := strings.TrimSpace(scanner.Text()); v != "" {
|
||||
user.Role = resolvespec_common.NewSqlString(v)
|
||||
}
|
||||
|
||||
user.UpdatedAt = resolvespec_common.NewSqlTimeStamp(time.Now())
|
||||
|
||||
_, err := storage.DB.NewUpdate().Model(user).
|
||||
Set("username = ?", user.Username.String()).
|
||||
Set("email = ?", user.Email.String()).
|
||||
@@ -333,9 +409,9 @@ func deleteUser(username string) {
|
||||
user := findUserByUsername(username)
|
||||
|
||||
fmt.Printf("Delete user '%s'? This cannot be undone. [y/N]: ", username)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
if strings.ToLower(strings.TrimSpace(scanner.Text())) != "y" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
line, _ := reader.ReadString('\n')
|
||||
if strings.ToLower(strings.TrimSpace(line)) != "y" {
|
||||
fmt.Println("Cancelled")
|
||||
return
|
||||
}
|
||||
@@ -356,14 +432,63 @@ func findUserByUsername(username string) *models.ModelPublicUsers {
|
||||
return &user
|
||||
}
|
||||
|
||||
func readPassword(prompt string) string {
|
||||
fmt.Print(prompt)
|
||||
pw, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
return strings.TrimSpace(scanner.Text())
|
||||
// promptLine prints a prompt and reads one line. Returns defaultVal if Enter pressed with no input.
|
||||
func promptLine(label, defaultVal string) string {
|
||||
if defaultVal != "" {
|
||||
fmt.Printf("%s [%s]: ", label, defaultVal)
|
||||
} else {
|
||||
fmt.Printf("%s: ", label)
|
||||
}
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return defaultVal
|
||||
}
|
||||
v := strings.TrimRight(line, "\r\n")
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// promptRequired loops until a non-empty value is entered.
|
||||
func promptRequired(label string) string {
|
||||
for {
|
||||
v := promptLine(label, "")
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
fmt.Printf("%s is required, please try again.\n", label)
|
||||
}
|
||||
}
|
||||
|
||||
// readPassword reads a password with hidden input when on a TTY, plain text otherwise.
|
||||
// Loops until a non-empty value is entered.
|
||||
func readPassword(prompt string) string {
|
||||
fd := int(os.Stdin.Fd())
|
||||
isTTY := term.IsTerminal(fd)
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
var pw string
|
||||
if isTTY {
|
||||
p, err := term.ReadPassword(fd)
|
||||
fmt.Println()
|
||||
if err == nil {
|
||||
pw = string(p)
|
||||
}
|
||||
} else {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
p, err := reader.ReadString('\n')
|
||||
fmt.Println()
|
||||
if err != nil && err != io.EOF {
|
||||
fmt.Fprintln(os.Stderr, "Error: cannot read password interactively. Use set-password <username> <password> instead.")
|
||||
os.Exit(1)
|
||||
}
|
||||
pw = strings.TrimRight(p, "\r\n")
|
||||
}
|
||||
if pw != "" {
|
||||
return pw
|
||||
}
|
||||
fmt.Println("Password cannot be empty, please try again.")
|
||||
}
|
||||
return string(pw)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user