From ed2ff62fd23e316c2f876ec039a2e85d7d9b6fda Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 12 Aug 2020 18:29:14 +0200 Subject: [PATCH 01/15] Started adding user list command Signed-off-by: kolaente --- pkg/cmd/user.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pkg/cmd/user.go diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go new file mode 100644 index 000000000..1910b6750 --- /dev/null +++ b/pkg/cmd/user.go @@ -0,0 +1,52 @@ +// Copyright 2020 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Vikunja is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Vikunja. If not, see . + +package cmd + +import ( + "code.vikunja.io/api/pkg/initialize" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/user" + "fmt" + "github.com/spf13/cobra" +) + +func init() { + userCmd.AddCommand(userListCmd) + rootCmd.AddCommand(userCmd) +} + +var userCmd = &cobra.Command{ + Use: "user", + Short: "Manage users locally through the cli.", +} + +var userListCmd = &cobra.Command{ + Use: "list", + Short: "Shows a list of all users on this instance.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + users, err := user.ListUsers("") + if err != nil { + log.Criticalf("Error getting users: %s", err) + } + // TODO: Pretty print as ascii tables (only relevant stuff, exlude things like passwords + fmt.Println(users) + }, +} -- 2.40.1 From 99b0d311dd34f898255a6542cc069f546e2a0768 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 12:18:25 +0200 Subject: [PATCH 02/15] Render users in a beautiful table Signed-off-by: kolaente --- pkg/cmd/user.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 1910b6750..0992d299a 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -21,8 +21,11 @@ import ( "code.vikunja.io/api/pkg/initialize" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" - "fmt" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + "os" + "strconv" + "time" ) func init() { @@ -46,7 +49,28 @@ var userListCmd = &cobra.Command{ if err != nil { log.Criticalf("Error getting users: %s", err) } - // TODO: Pretty print as ascii tables (only relevant stuff, exlude things like passwords - fmt.Println(users) + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{ + "ID", + "Username", + "Email", + "Active", + "Created", + "Updated", + }) + + for _, u := range users { + table.Append([]string{ + strconv.FormatInt(u.ID, 10), + u.Username, + u.Email, + strconv.FormatBool(u.IsActive), + u.Created.Format(time.RFC3339), + u.Updated.Format(time.RFC3339), + }) + } + + table.Render() }, } -- 2.40.1 From efa052f8ad5a631eee64b465a50d42cf9ef6a7f2 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 12:25:22 +0200 Subject: [PATCH 03/15] Add command stubs for all commands Signed-off-by: kolaente --- pkg/cmd/user.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 0992d299a..d04a661f3 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -29,7 +29,7 @@ import ( ) func init() { - userCmd.AddCommand(userListCmd) + userCmd.AddCommand(userListCmd, userCreateCmd, userDeleteCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) } @@ -40,7 +40,7 @@ var userCmd = &cobra.Command{ var userListCmd = &cobra.Command{ Use: "list", - Short: "Shows a list of all users on this instance.", + Short: "Shows a list of all users.", PreRun: func(cmd *cobra.Command, args []string) { initialize.FullInit() }, @@ -74,3 +74,58 @@ var userListCmd = &cobra.Command{ table.Render() }, } + +var userCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new user.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var userUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing user.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var userDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a user.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + + }, +} + +var userResetPasswordCmd = &cobra.Command{ + Use: "reset-password", + Short: "Reset a users password, either through mailing them a reset link or directly.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + // need flags + }, +} + +var userChangeEnabledCmd = &cobra.Command{ + Use: "toggle-status", + Short: "Enable or disable a user.", + PreRun: func(cmd *cobra.Command, args []string) { + initialize.FullInit() + }, + Run: func(cmd *cobra.Command, args []string) { + // Flag to either enable or disable + }, +} -- 2.40.1 From b09df253246803f302eb9647d21f61383b3503c9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 12:47:37 +0200 Subject: [PATCH 04/15] Add user create command Signed-off-by: kolaente --- pkg/cmd/user.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index d04a661f3..17b7fcd45 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -21,14 +21,31 @@ import ( "code.vikunja.io/api/pkg/initialize" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" + "fmt" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "os" "strconv" + "strings" + "syscall" "time" ) +var ( + userCreateFlagUsername string + userCreateFlagEmail string + userCreateFlagPassword string +) + func init() { + // User create flags + userCreateCmd.Flags().StringVarP(&userCreateFlagUsername, "username", "u", "", "The username of the new user.") + _ = userCreateCmd.MarkFlagRequired("username") + userCreateCmd.Flags().StringVarP(&userCreateFlagEmail, "email", "e", "", "The email address of the new user.") + _ = userCreateCmd.MarkFlagRequired("email") + userCreateCmd.Flags().StringVarP(&userCreateFlagPassword, "password", "p", "", "The password of the new user. You will be asked to enter it if not provided through the flag.") + userCmd.AddCommand(userListCmd, userCreateCmd, userDeleteCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) } @@ -83,6 +100,34 @@ var userCreateCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { + if userCreateFlagPassword == "" { + fmt.Print("Enter Password: ") + bytePW, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + log.Criticalf("Error reading password: %s", err) + } + fmt.Printf("\nConfirm Password: ") + byteConfirmPW, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + log.Criticalf("Error reading password: %s", err) + } + if string(bytePW) != string(byteConfirmPW) { + log.Critical("Passwords don't match!") + } + userCreateFlagPassword = strings.TrimSpace(string(bytePW)) + } + + u := &user.User{ + Username: userCreateFlagUsername, + Email: userCreateFlagEmail, + Password: userCreateFlagPassword, + } + _, err := user.CreateUser(u) + if err != nil { + log.Criticalf("Error creating new user: %s", err) + } + + fmt.Printf("\nUser was created successfully.\n") }, } -- 2.40.1 From 48868983e8bbc58f8395c28150f1404e0c9cc2ea Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 12:48:47 +0200 Subject: [PATCH 05/15] Rename flags Signed-off-by: kolaente --- pkg/cmd/user.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 17b7fcd45..97a4dc8b0 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -33,18 +33,18 @@ import ( ) var ( - userCreateFlagUsername string - userCreateFlagEmail string - userCreateFlagPassword string + userFlagUsername string + userFlagEmail string + userFlagPassword string ) func init() { // User create flags - userCreateCmd.Flags().StringVarP(&userCreateFlagUsername, "username", "u", "", "The username of the new user.") + userCreateCmd.Flags().StringVarP(&userFlagUsername, "username", "u", "", "The username of the new user.") _ = userCreateCmd.MarkFlagRequired("username") - userCreateCmd.Flags().StringVarP(&userCreateFlagEmail, "email", "e", "", "The email address of the new user.") + userCreateCmd.Flags().StringVarP(&userFlagEmail, "email", "e", "", "The email address of the new user.") _ = userCreateCmd.MarkFlagRequired("email") - userCreateCmd.Flags().StringVarP(&userCreateFlagPassword, "password", "p", "", "The password of the new user. You will be asked to enter it if not provided through the flag.") + userCreateCmd.Flags().StringVarP(&userFlagPassword, "password", "p", "", "The password of the new user. You will be asked to enter it if not provided through the flag.") userCmd.AddCommand(userListCmd, userCreateCmd, userDeleteCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) @@ -100,7 +100,7 @@ var userCreateCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { - if userCreateFlagPassword == "" { + if userFlagPassword == "" { fmt.Print("Enter Password: ") bytePW, err := terminal.ReadPassword(syscall.Stdin) if err != nil { @@ -114,13 +114,13 @@ var userCreateCmd = &cobra.Command{ if string(bytePW) != string(byteConfirmPW) { log.Critical("Passwords don't match!") } - userCreateFlagPassword = strings.TrimSpace(string(bytePW)) + userFlagPassword = strings.TrimSpace(string(bytePW)) } u := &user.User{ - Username: userCreateFlagUsername, - Email: userCreateFlagEmail, - Password: userCreateFlagPassword, + Username: userFlagUsername, + Email: userFlagEmail, + Password: userFlagPassword, } _, err := user.CreateUser(u) if err != nil { -- 2.40.1 From 1b96a4ab2decc4cc943fcaa1753d4b186bf1e9e7 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 13:03:17 +0200 Subject: [PATCH 06/15] Add user update command Signed-off-by: kolaente --- pkg/cmd/user.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 97a4dc8b0..4408a244f 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -36,6 +36,7 @@ var ( userFlagUsername string userFlagEmail string userFlagPassword string + userFlagAvatar = "default" ) func init() { @@ -45,6 +46,12 @@ func init() { userCreateCmd.Flags().StringVarP(&userFlagEmail, "email", "e", "", "The email address of the new user.") _ = userCreateCmd.MarkFlagRequired("email") userCreateCmd.Flags().StringVarP(&userFlagPassword, "password", "p", "", "The password of the new user. You will be asked to enter it if not provided through the flag.") + userCreateCmd.Flags().StringVarP(&userFlagAvatar, "avatar-provider", "a", "", "The avatar provider of the new user. Optional.") + + // User update flags + userUpdateCmd.Flags().StringVarP(&userFlagUsername, "username", "u", "", "The new username of the user.") + userUpdateCmd.Flags().StringVarP(&userFlagEmail, "email", "e", "", "The new email address of the user.") + userUpdateCmd.Flags().StringVarP(&userFlagAvatar, "avatar-provider", "a", "", "The new avatar provider of the new user.") userCmd.AddCommand(userListCmd, userCreateCmd, userDeleteCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) @@ -134,11 +141,37 @@ var userCreateCmd = &cobra.Command{ var userUpdateCmd = &cobra.Command{ Use: "update", Short: "Update an existing user.", + Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { initialize.FullInit() }, Run: func(cmd *cobra.Command, args []string) { + id, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + log.Criticalf("Invalid user id: %s", err) + } + u, err := user.GetUserByID(id) + if err != nil { + log.Criticalf("Could not get user: %s", err) + } + + if userFlagUsername != "" { + u.Username = userFlagUsername + } + if userFlagEmail != "" { + u.Email = userFlagEmail + } + if userFlagAvatar != "default" { + u.AvatarProvider = userFlagAvatar + } + + _, err = user.UpdateUser(u) + if err != nil { + log.Criticalf("Error updating the user: %s", err) + } + + fmt.Println("User updated successfully.") }, } -- 2.40.1 From 458a6645ee1177d8d10c437ccb58cab7ba7ad0cc Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:15:04 +0200 Subject: [PATCH 07/15] Fail if changing the username would result in duplicate users --- pkg/cmd/user.go | 2 +- pkg/user/user.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 4408a244f..975aff837 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -168,7 +168,7 @@ var userUpdateCmd = &cobra.Command{ _, err = user.UpdateUser(u) if err != nil { - log.Criticalf("Error updating the user: %s", err) + log.Fatalf("Error updating the user: %s", err) } fmt.Println("User updated successfully.") diff --git a/pkg/user/user.go b/pkg/user/user.go index 15eac8e95..3683cce42 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -324,6 +324,15 @@ func UpdateUser(user *User) (updatedUser *User, err error) { if user.Username == "" { //return User{}, ErrNoUsername{user.ID} user.Username = theUser.Username // Dont change the username if we dont have one + } else { + // Check if the new username already exists + uu, err := GetUserByUsername(user.Username) + if err != nil && !IsErrUserDoesNotExist(err) { + return nil, err + } + if uu.ID != user.ID { + return nil, &ErrUsernameExists{Username: user.Username, UserID: uu.ID} + } } user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it -- 2.40.1 From ca6df2095e43c08554cf81204a1bfb8ee89ea476 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:15:27 +0200 Subject: [PATCH 08/15] Make sure to fail on any errors --- pkg/cmd/user.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 975aff837..0c0feca77 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -71,7 +71,7 @@ var userListCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { users, err := user.ListUsers("") if err != nil { - log.Criticalf("Error getting users: %s", err) + log.Fatalf("Error getting users: %s", err) } table := tablewriter.NewWriter(os.Stdout) @@ -111,12 +111,12 @@ var userCreateCmd = &cobra.Command{ fmt.Print("Enter Password: ") bytePW, err := terminal.ReadPassword(syscall.Stdin) if err != nil { - log.Criticalf("Error reading password: %s", err) + log.Fatalf("Error reading password: %s", err) } fmt.Printf("\nConfirm Password: ") byteConfirmPW, err := terminal.ReadPassword(syscall.Stdin) if err != nil { - log.Criticalf("Error reading password: %s", err) + log.Fatalf("Error reading password: %s", err) } if string(bytePW) != string(byteConfirmPW) { log.Critical("Passwords don't match!") @@ -131,7 +131,7 @@ var userCreateCmd = &cobra.Command{ } _, err := user.CreateUser(u) if err != nil { - log.Criticalf("Error creating new user: %s", err) + log.Fatalf("Error creating new user: %s", err) } fmt.Printf("\nUser was created successfully.\n") @@ -148,12 +148,12 @@ var userUpdateCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { - log.Criticalf("Invalid user id: %s", err) + log.Fatalf("Invalid user id: %s", err) } u, err := user.GetUserByID(id) if err != nil { - log.Criticalf("Could not get user: %s", err) + log.Fatalf("Could not get user: %s", err) } if userFlagUsername != "" { -- 2.40.1 From 4de436624d7b4af146a9ed3064c2e66c121d92dc Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:16:44 +0200 Subject: [PATCH 09/15] Remove user delete (too many possible side effects, postponed until later) --- pkg/cmd/user.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 0c0feca77..1c0233b0c 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -53,7 +53,7 @@ func init() { userUpdateCmd.Flags().StringVarP(&userFlagEmail, "email", "e", "", "The new email address of the user.") userUpdateCmd.Flags().StringVarP(&userFlagAvatar, "avatar-provider", "a", "", "The new avatar provider of the new user.") - userCmd.AddCommand(userListCmd, userCreateCmd, userDeleteCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) + userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) } @@ -175,17 +175,6 @@ var userUpdateCmd = &cobra.Command{ }, } -var userDeleteCmd = &cobra.Command{ - Use: "delete", - Short: "Delete a user.", - PreRun: func(cmd *cobra.Command, args []string) { - initialize.FullInit() - }, - Run: func(cmd *cobra.Command, args []string) { - - }, -} - var userResetPasswordCmd = &cobra.Command{ Use: "reset-password", Short: "Reset a users password, either through mailing them a reset link or directly.", -- 2.40.1 From caa6cf81f8cf12f56645c5c3016291a9d94e9643 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:17:39 +0200 Subject: [PATCH 10/15] Add user id to help --- pkg/cmd/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 1c0233b0c..c4f23a114 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -139,7 +139,7 @@ var userCreateCmd = &cobra.Command{ } var userUpdateCmd = &cobra.Command{ - Use: "update", + Use: "update [user id]", Short: "Update an existing user.", Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { -- 2.40.1 From 922e688720ddc1c4fb2e6765eb5c2ce8f6868587 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:36:25 +0200 Subject: [PATCH 11/15] Add password reset --- pkg/cmd/user.go | 100 +++++++++++++++-------- pkg/routes/api/v1/user_password_reset.go | 2 +- pkg/user/user_password_reset.go | 9 +- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index c4f23a114..84b18f269 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -33,10 +33,11 @@ import ( ) var ( - userFlagUsername string - userFlagEmail string - userFlagPassword string - userFlagAvatar = "default" + userFlagUsername string + userFlagEmail string + userFlagPassword string + userFlagAvatar = "default" + userFlagResetPasswordDirectly bool ) func init() { @@ -53,10 +54,49 @@ func init() { userUpdateCmd.Flags().StringVarP(&userFlagEmail, "email", "e", "", "The new email address of the user.") userUpdateCmd.Flags().StringVarP(&userFlagAvatar, "avatar-provider", "a", "", "The new avatar provider of the new user.") + // Reset PW flags + userResetPasswordCmd.Flags().BoolVarP(&userFlagResetPasswordDirectly, "direct", "d", false, "If provided, reset the password directly instead of sending the user a reset mail.") + userResetPasswordCmd.Flags().StringVarP(&userFlagPassword, "password", "p", "", "The new password of the user. Only used in combination with --direct. You will be asked to enter it if not provided through the flag.") + userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) } +func getPasswordFromFlagOrInput() (pw string) { + pw = userFlagPassword + if userFlagPassword == "" { + fmt.Print("Enter Password: ") + bytePW, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + log.Fatalf("Error reading password: %s", err) + } + fmt.Printf("\nConfirm Password: ") + byteConfirmPW, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + log.Fatalf("Error reading password: %s", err) + } + if string(bytePW) != string(byteConfirmPW) { + log.Critical("Passwords don't match!") + } + fmt.Printf("\n") + pw = strings.TrimSpace(string(bytePW)) + } + return +} + +func getUserFromArg(arg string) *user.User { + id, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + log.Fatalf("Invalid user id: %s", err) + } + + u, err := user.GetUserByID(id) + if err != nil { + log.Fatalf("Could not get user: %s", err) + } + return u +} + var userCmd = &cobra.Command{ Use: "user", Short: "Manage users locally through the cli.", @@ -106,28 +146,10 @@ var userCreateCmd = &cobra.Command{ initialize.FullInit() }, Run: func(cmd *cobra.Command, args []string) { - - if userFlagPassword == "" { - fmt.Print("Enter Password: ") - bytePW, err := terminal.ReadPassword(syscall.Stdin) - if err != nil { - log.Fatalf("Error reading password: %s", err) - } - fmt.Printf("\nConfirm Password: ") - byteConfirmPW, err := terminal.ReadPassword(syscall.Stdin) - if err != nil { - log.Fatalf("Error reading password: %s", err) - } - if string(bytePW) != string(byteConfirmPW) { - log.Critical("Passwords don't match!") - } - userFlagPassword = strings.TrimSpace(string(bytePW)) - } - u := &user.User{ Username: userFlagUsername, Email: userFlagEmail, - Password: userFlagPassword, + Password: getPasswordFromFlagOrInput(), } _, err := user.CreateUser(u) if err != nil { @@ -146,15 +168,7 @@ var userUpdateCmd = &cobra.Command{ initialize.FullInit() }, Run: func(cmd *cobra.Command, args []string) { - id, err := strconv.ParseInt(args[0], 10, 64) - if err != nil { - log.Fatalf("Invalid user id: %s", err) - } - - u, err := user.GetUserByID(id) - if err != nil { - log.Fatalf("Could not get user: %s", err) - } + u := getUserFromArg(args[0]) if userFlagUsername != "" { u.Username = userFlagUsername @@ -166,7 +180,7 @@ var userUpdateCmd = &cobra.Command{ u.AvatarProvider = userFlagAvatar } - _, err = user.UpdateUser(u) + _, err := user.UpdateUser(u) if err != nil { log.Fatalf("Error updating the user: %s", err) } @@ -176,13 +190,29 @@ var userUpdateCmd = &cobra.Command{ } var userResetPasswordCmd = &cobra.Command{ - Use: "reset-password", + Use: "reset-password [user id]", Short: "Reset a users password, either through mailing them a reset link or directly.", PreRun: func(cmd *cobra.Command, args []string) { initialize.FullInit() }, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - // need flags + u := getUserFromArg(args[0]) + + // By default we reset as usual, only with specific flag directly. + if userFlagResetPasswordDirectly { + err := user.UpdateUserPassword(u, getPasswordFromFlagOrInput()) + if err != nil { + log.Fatalf("Could not update user password: %s", err) + } + fmt.Println("Password updated successfully.") + } else { + err := user.RequestUserPasswordResetToken(u) + if err != nil { + log.Fatalf("Could not send password reset email: %s", err) + } + fmt.Println("Password reset email sent successfully.") + } }, } diff --git a/pkg/routes/api/v1/user_password_reset.go b/pkg/routes/api/v1/user_password_reset.go index 84cdce6ed..10b1af0ff 100644 --- a/pkg/routes/api/v1/user_password_reset.go +++ b/pkg/routes/api/v1/user_password_reset.go @@ -72,7 +72,7 @@ func UserRequestResetPasswordToken(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err) } - err := user.RequestUserPasswordResetToken(&pwTokenReset) + err := user.RequestUserPasswordResetTokenByEmail(&pwTokenReset) if err != nil { return handler.HandleHTTPError(err, c) } diff --git a/pkg/user/user_password_reset.go b/pkg/user/user_password_reset.go index 24d47d2b5..4ec892e07 100644 --- a/pkg/user/user_password_reset.go +++ b/pkg/user/user_password_reset.go @@ -82,8 +82,8 @@ type PasswordTokenRequest struct { Email string `json:"email" valid:"email,length(0|250)" maxLength:"250"` } -// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse -func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) { +// RequestUserPasswordResetTokenByEmail inserts a random token to reset a users password into the databsse +func RequestUserPasswordResetTokenByEmail(tr *PasswordTokenRequest) (err error) { if tr.Email == "" { return ErrNoUsernamePassword{} } @@ -94,6 +94,11 @@ func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) { return } + return RequestUserPasswordResetToken(user) +} + +// RequestUserPasswordResetToken sends a user a password reset email. +func RequestUserPasswordResetToken(user *User) (err error) { // Generate a token and save it user.PasswordResetToken = utils.MakeRandomString(400) -- 2.40.1 From 594baa921b841de407126cfcefef9ccd2b5c1ca9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:41:29 +0200 Subject: [PATCH 12/15] Make sure only one user exists with a particular email when updating --- pkg/user/user.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/user/user.go b/pkg/user/user.go index 3683cce42..6d152b701 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -335,6 +335,19 @@ func UpdateUser(user *User) (updatedUser *User, err error) { } } + // Check if the email is already used + if user.Email == "" { + user.Email = theUser.Email + } else { + uu, err := getUser(&User{Email: user.Email}, true) + if err != nil && !IsErrUserDoesNotExist(err) { + return nil, err + } + if uu.ID != user.ID { + return nil, &ErrUserEmailExists{Email: user.Email, UserID: uu.ID} + } + } + user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it // Validate the avatar type -- 2.40.1 From 31c61d7febf36ddc2372d81307d76c5404a5ade3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 16:57:11 +0200 Subject: [PATCH 13/15] Add user status change command --- pkg/cmd/user.go | 27 ++++++++++++++++++++++++--- pkg/user/user.go | 9 +++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 84b18f269..ee1e40540 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -38,6 +38,8 @@ var ( userFlagPassword string userFlagAvatar = "default" userFlagResetPasswordDirectly bool + userFlagEnableUser bool + userFlagDisableUser bool ) func init() { @@ -58,6 +60,10 @@ func init() { userResetPasswordCmd.Flags().BoolVarP(&userFlagResetPasswordDirectly, "direct", "d", false, "If provided, reset the password directly instead of sending the user a reset mail.") userResetPasswordCmd.Flags().StringVarP(&userFlagPassword, "password", "p", "", "The new password of the user. Only used in combination with --direct. You will be asked to enter it if not provided through the flag.") + // Change status flags + userChangeEnabledCmd.Flags().BoolVarP(&userFlagDisableUser, "disable", "d", false, "Disable the user.") + userChangeEnabledCmd.Flags().BoolVarP(&userFlagEnableUser, "enable", "e", false, "Enable the user.") + userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd) rootCmd.AddCommand(userCmd) } @@ -217,12 +223,27 @@ var userResetPasswordCmd = &cobra.Command{ } var userChangeEnabledCmd = &cobra.Command{ - Use: "toggle-status", - Short: "Enable or disable a user.", + Use: "change-status [user id]", + Short: "Enable or disable a user. Will toggle the current status if no flag (--enable or --disable) is provided.", PreRun: func(cmd *cobra.Command, args []string) { initialize.FullInit() }, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - // Flag to either enable or disable + u := getUserFromArg(args[0]) + + if userFlagEnableUser { + u.IsActive = true + } else if userFlagDisableUser { + u.IsActive = false + } else { + u.IsActive = !u.IsActive + } + _, err := user.UpdateUser(u) + if err != nil { + log.Fatalf("Could not enable the user") + } + + fmt.Printf("User status successfully changed, user is now active: %t.\n", u.IsActive) }, } diff --git a/pkg/user/user.go b/pkg/user/user.go index 6d152b701..9b32db033 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -315,7 +315,7 @@ func hashPassword(password string) (string, error) { func UpdateUser(user *User) (updatedUser *User, err error) { // Check if it exists - theUser, err := GetUserByID(user.ID) + theUser, err := GetUserWithEmail(&User{ID: user.ID}) if err != nil { return &User{}, err } @@ -348,8 +348,6 @@ func UpdateUser(user *User) (updatedUser *User, err error) { } } - user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it - // Validate the avatar type if user.AvatarProvider != "" { if user.AvatarProvider != "default" && @@ -361,7 +359,10 @@ func UpdateUser(user *User) (updatedUser *User, err error) { } // Update it - _, err = x.ID(user.ID).Update(user) + _, err = x. + ID(user.ID). + Cols("username", "email", "avatar_provider", "is_active"). + Update(user) if err != nil { return &User{}, err } -- 2.40.1 From 93c9b9c291da059ab18c5ee4340a80ebe12fe250 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 17:02:11 +0200 Subject: [PATCH 14/15] Fix checking for changing username or user email --- pkg/user/user.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/user/user.go b/pkg/user/user.go index 9b32db033..7dec28f38 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -330,7 +330,7 @@ func UpdateUser(user *User) (updatedUser *User, err error) { if err != nil && !IsErrUserDoesNotExist(err) { return nil, err } - if uu.ID != user.ID { + if uu.ID != 0 && uu.ID != user.ID { return nil, &ErrUsernameExists{Username: user.Username, UserID: uu.ID} } } @@ -343,7 +343,7 @@ func UpdateUser(user *User) (updatedUser *User, err error) { if err != nil && !IsErrUserDoesNotExist(err) { return nil, err } - if uu.ID != user.ID { + if uu.ID != 0 && uu.ID != user.ID { return nil, &ErrUserEmailExists{Email: user.Email, UserID: uu.ID} } } -- 2.40.1 From 5e0ce4641c7c88e9ca42c44112a32b6d9f26554d Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 13 Aug 2020 17:22:39 +0200 Subject: [PATCH 15/15] Add users cli commands to docs --- docs/content/doc/usage/cli.md | 71 ++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/content/doc/usage/cli.md b/docs/content/doc/usage/cli.md index 8de09d905..a32cf7944 100644 --- a/docs/content/doc/usage/cli.md +++ b/docs/content/doc/usage/cli.md @@ -18,6 +18,7 @@ The following commands are available: * [migrate](#migrate) * [restore](#restore) * [testmail](#testmail) +* [user](#user) * [version](#version) * [web](#web) @@ -85,7 +86,7 @@ Usage: $ vikunja restore {{< /highlight >}} -### testmail +### `testmail` Sends a test mail using the configured smtp connection. @@ -94,6 +95,74 @@ Usage: $ vikunja testmail {{< /highlight >}} +### `user` + +Bundles a few commands to manage users. + +#### `user change-status` + +Enable or disable a user. Will toggle the current status if no flag (`--enable` or `--disable`) is provided. + +Usage: +{{< highlight bash >}} +$ vikunja user change-status +{{< /highlight >}} + +Flags: +* `-d`, `--disable`: Disable the user. +* `-e`, `--enable`: Enable the user. + +#### `user create` + +Create a new user. + +Usage: +{{< highlight bash >}} +$ vikunja user create +{{< /highlight >}} + +Flags: +* `-a`, `--avatar-provider`: The avatar provider of the new user. Optional. +* `-e`, `--email`: The email address of the new user. +* `-p`, `--password`: The password of the new user. You will be asked to enter it if not provided through the flag. +* `-u`, `--username`: The username of the new user. + +#### `user list` + +Shows a list of all users. + +Usage: +{{< highlight bash >}} +$ vikunja user list +{{< /highlight >}} + +#### `user reset-password` + +Reset a users password, either through mailing them a reset link or directly. + +Usage: +{{< highlight bash >}} +$ vikunja user reset-password +{{< /highlight >}} + +Flags: +* `-d`, `--direct`: If provided, reset the password directly instead of sending the user a reset mail. +* `-p`, `--password`: The new password of the user. Only used in combination with --direct. You will be asked to enter it if not provided through the flag. + +#### `user update` + +Update an existing user. + +Usage: +{{< highlight bash >}} +$ vikunja user update +{{< /highlight >}} + +Flags: +* `-a`, `--avatar-provider`: The new avatar provider of the new user. +* `-e`, `--email`: The new email address of the user. +* `-u`, `--username`: The new username of the user. + ### `version` Prints the version of Vikunja. -- 2.40.1