photoprism/internal/commands/users.go
Michael Mayer 85561547cc Auth: Add "PHOTOPRISM_ADMIN_USER" option and refactor user table #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
2022-09-02 21:30:50 +02:00

292 lines
6.3 KiB
Go

package commands
import (
"context"
"errors"
"fmt"
"strings"
"github.com/dustin/go-humanize/english"
"github.com/manifoldco/promptui"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/report"
)
const UsernameUsage = "unique login identifier"
const EmailUsage = "unique email address"
const PasswordUsage = "secure login password"
// UsersCommand registers user management subcommands.
var UsersCommand = cli.Command{
Name: "users",
Usage: "User management subcommands",
Subcommands: []cli.Command{
{
Name: "ls",
Aliases: []string{"list"},
Usage: "Shows registered users",
Flags: report.CliFlags,
Action: usersListAction,
},
{
Name: "add",
Aliases: []string{"create"},
Usage: "Adds a new user account",
Action: usersAddAction,
Flags: []cli.Flag{
cli.StringFlag{
Name: "username, u",
Usage: UsernameUsage,
},
cli.StringFlag{
Name: "email, m",
Usage: EmailUsage,
},
cli.StringFlag{
Name: "password, p",
Usage: PasswordUsage,
},
},
},
{
Name: "mod",
Aliases: []string{"update"},
Usage: "Updates a user account",
Action: usersUpdateAction,
Flags: []cli.Flag{
cli.StringFlag{
Name: "username, u",
Usage: UsernameUsage,
},
cli.StringFlag{
Name: "email, m",
Usage: EmailUsage,
},
cli.StringFlag{
Name: "password, p",
Usage: PasswordUsage,
},
},
},
{
Name: "rm",
Aliases: []string{"delete"},
Usage: "Removes a user account",
Action: usersDeleteAction,
ArgsUsage: "[username]",
},
},
}
func usersAddAction(ctx *cli.Context) error {
return callWithDependencies(ctx, func(conf *config.Config) error {
uc := form.UserCreate{
Username: strings.TrimSpace(ctx.String("username")),
Email: strings.TrimSpace(ctx.String("email")),
Password: strings.TrimSpace(ctx.String("password")),
}
interactive := true
if uc.Username != "" && uc.Password != "" {
log.Debugf("creating user in non-interactive mode")
interactive = false
}
if interactive && uc.Username == "" {
prompt := promptui.Prompt{
Label: "Username",
}
res, err := prompt.Run()
if err != nil {
return err
}
uc.Username = strings.TrimSpace(res)
}
if interactive && uc.Email == "" {
prompt := promptui.Prompt{
Label: "Email",
}
res, err := prompt.Run()
if err != nil {
return err
}
uc.Email = strings.TrimSpace(res)
}
if interactive && len(ctx.String("password")) < entity.PasswordLength {
validate := func(input string) error {
if len(input) < entity.PasswordLength {
return fmt.Errorf("password must have at least %d characters", entity.PasswordLength)
}
return nil
}
prompt := promptui.Prompt{
Label: "Password",
Validate: validate,
Mask: '*',
}
resPasswd, err := prompt.Run()
if err != nil {
return err
}
validateRetype := func(input string) error {
if input != resPasswd {
return errors.New("password does not match")
}
return nil
}
confirm := promptui.Prompt{
Label: "Retype Password",
Validate: validateRetype,
Mask: '*',
}
resConfirm, err := confirm.Run()
if err != nil {
return err
}
if resConfirm != resPasswd {
return errors.New("passwords did not match or too short. please try again")
} else {
uc.Password = resPasswd
}
}
if err := entity.CreateWithPassword(uc); err != nil {
return err
}
return nil
})
}
func usersDeleteAction(ctx *cli.Context) error {
return callWithDependencies(ctx, func(conf *config.Config) error {
login := strings.TrimSpace(ctx.Args().First())
if login == "" {
return errors.New("please provide a username")
}
actionPrompt := promptui.Prompt{
Label: fmt.Sprintf("Delete %s?", clean.Log(login)),
IsConfirm: true,
}
if _, err := actionPrompt.Run(); err == nil {
if m := entity.FindUserByLogin(login); m == nil {
return errors.New("login name not found")
} else if err := m.Delete(); err != nil {
return err
} else {
log.Infof("%s deleted", clean.LogQuote(login))
}
} else {
log.Infof("keeping user")
}
return nil
})
}
func usersListAction(ctx *cli.Context) error {
return callWithDependencies(ctx, func(conf *config.Config) error {
cols := []string{"UID", "Role", "Username", "Email", "Display Name"}
users := query.RegisteredUsers()
rows := make([][]string, len(users))
log.Infof("found %s", english.Plural(len(users), "user", "users"))
for i, user := range users {
rows[i] = []string{user.UserUID, user.AclRole().String(), user.UserName(), user.UserEmail(), user.RealName()}
}
result, err := report.Render(rows, cols, report.CliFormat(ctx))
fmt.Println(result)
return err
})
}
func usersUpdateAction(ctx *cli.Context) error {
return callWithDependencies(ctx, func(conf *config.Config) error {
login := ctx.Args().First()
if login == "" {
return errors.New("please provide the username as argument")
}
u := entity.FindUserByLogin(login)
if u == nil {
return errors.New("username not found")
}
uc := form.UserCreate{
Username: strings.TrimSpace(ctx.String("username")),
Email: strings.TrimSpace(ctx.String("email")),
Password: strings.TrimSpace(ctx.String("password")),
}
if ctx.IsSet("email") && len(uc.Email) > 0 {
u.Email = uc.Email
}
if ctx.IsSet("password") {
err := u.SetPassword(uc.Password)
if err != nil {
return err
}
fmt.Printf("password successfully changed: %s\n", clean.Log(u.UserName()))
}
if err := u.Validate(); err != nil {
return err
}
if err := u.Save(); err != nil {
return err
}
fmt.Printf("user account successfully updated: %s\n", clean.Log(u.UserName()))
return nil
})
}
func callWithDependencies(ctx *cli.Context, f func(conf *config.Config) error) error {
conf := config.NewConfig(ctx)
_, cancel := context.WithCancel(context.Background())
defer cancel()
if err := conf.Init(); err != nil {
return err
}
conf.InitDb()
defer conf.Shutdown()
// Run command.
if err := f(conf); err != nil {
return err
}
return nil
}