Manage users via cli #632
|
@ -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 <path to dump zip file>
|
||||
{{< /highlight >}}
|
||||
|
||||
### testmail
|
||||
### `testmail`
|
||||
|
||||
Sends a test mail using the configured smtp connection.
|
||||
|
||||
|
@ -94,6 +95,74 @@ Usage:
|
|||
$ vikunja testmail <email to send the test mail to>
|
||||
{{< /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 <user id> <flags>
|
||||
{{< /highlight >}}
|
||||
|
||||
Flags:
|
||||
* `-d`, `--disable`: Disable the user.
|
||||
* `-e`, `--enable`: Enable the user.
|
||||
|
||||
#### `user create`
|
||||
|
||||
Create a new user.
|
||||
|
||||
Usage:
|
||||
{{< highlight bash >}}
|
||||
$ vikunja user create <flags>
|
||||
{{< /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 <flags>
|
||||
{{< /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 <user id>
|
||||
{{< /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.
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
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 (
|
||||
userFlagUsername string
|
||||
userFlagEmail string
|
||||
userFlagPassword string
|
||||
userFlagAvatar = "default"
|
||||
userFlagResetPasswordDirectly bool
|
||||
userFlagEnableUser bool
|
||||
userFlagDisableUser bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
// User create flags
|
||||
userCreateCmd.Flags().StringVarP(&userFlagUsername, "username", "u", "", "The username of the new user.")
|
||||
_ = userCreateCmd.MarkFlagRequired("username")
|
||||
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.")
|
||||
|
||||
// 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.")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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.",
|
||||
}
|
||||
|
||||
var userListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Shows a list of all users.",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initialize.FullInit()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
users, err := user.ListUsers("")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting users: %s", err)
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
u := &user.User{
|
||||
Username: userFlagUsername,
|
||||
Email: userFlagEmail,
|
||||
Password: getPasswordFromFlagOrInput(),
|
||||
}
|
||||
_, err := user.CreateUser(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating new user: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nUser was created successfully.\n")
|
||||
},
|
||||
}
|
||||
|
||||
var userUpdateCmd = &cobra.Command{
|
||||
Use: "update [user id]",
|
||||
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) {
|
||||
u := getUserFromArg(args[0])
|
||||
|
||||
if userFlagUsername != "" {
|
||||
u.Username = userFlagUsername
|
||||
}
|
||||
if userFlagEmail != "" {
|
||||
u.Email = userFlagEmail
|
||||
}
|
||||
if userFlagAvatar != "default" {
|
||||
u.AvatarProvider = userFlagAvatar
|
||||
}
|
||||
|
||||
_, err := user.UpdateUser(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Error updating the user: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println("User updated successfully.")
|
||||
},
|
||||
}
|
||||
|
||||
var userResetPasswordCmd = &cobra.Command{
|
||||
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) {
|
||||
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.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var userChangeEnabledCmd = &cobra.Command{
|
||||
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) {
|
||||
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)
|
||||
},
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -324,9 +324,29 @@ 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 != 0 && 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
|
||||
// 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 != 0 && uu.ID != user.ID {
|
||||
return nil, &ErrUserEmailExists{Email: user.Email, UserID: uu.ID}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the avatar type
|
||||
if user.AvatarProvider != "" {
|
||||
|
@ -339,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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue