diff --git a/cmd/photoprism/photoprism.go b/cmd/photoprism/photoprism.go index c04b0de5b..5d25ce90f 100644 --- a/cmd/photoprism/photoprism.go +++ b/cmd/photoprism/photoprism.go @@ -70,10 +70,10 @@ func main() { commands.RestoreCommand, commands.ResetCommand, commands.ConfigCommand, + commands.UsersCommand, commands.PasswdCommand, commands.VersionCommand, commands.StatusCommand, - commands.UserCommand, } if err := app.Run(os.Args); err != nil { diff --git a/internal/commands/faces.go b/internal/commands/faces.go index 54b8f59ca..6638491ac 100644 --- a/internal/commands/faces.go +++ b/internal/commands/faces.go @@ -14,7 +14,7 @@ import ( // FacesCommand registers the faces cli command. var FacesCommand = cli.Command{ Name: "faces", - Usage: "Runs facial recognition commands", + Usage: "Facial recognition sub-commands", Subcommands: []cli.Command{ { Name: "stats", diff --git a/internal/commands/user.go b/internal/commands/users.go similarity index 70% rename from internal/commands/user.go rename to internal/commands/users.go index 3ff9d0e9c..50abf895a 100644 --- a/internal/commands/user.go +++ b/internal/commands/users.go @@ -8,6 +8,7 @@ import ( "os" "strings" + "github.com/manifoldco/promptui" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" @@ -16,15 +17,20 @@ import ( "github.com/urfave/cli" ) -// UserCommand Create, List, Update and Delete Users. -var UserCommand = cli.Command{ +// UsersCommand registers user management commands. +var UsersCommand = cli.Command{ Name: "users", - Usage: "Manage Users from CLI", + Usage: "User management sub-commands", Subcommands: []cli.Command{ + { + Name: "list", + Usage: "lists registered users", + Action: usersListAction, + }, { Name: "add", - Usage: "creates a new user. Provide at least username and password", - Action: userAdd, + Usage: "adds a new user", + Action: usersAddAction, Flags: []cli.Flag{ cli.StringFlag{ Name: "fullname, n", @@ -45,9 +51,9 @@ var UserCommand = cli.Command{ }, }, { - Name: "modify", - Usage: "modify a users information.", - Action: userModify, + Name: "update", + Usage: "updates user information", + Action: usersModifyAction, Flags: []cli.Flag{ cli.StringFlag{ Name: "fullname, n", @@ -69,26 +75,15 @@ var UserCommand = cli.Command{ }, { Name: "delete", - Usage: "deletes user by username", - Action: userDelete, - ArgsUsage: "takes username as argument", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "force, f", - Usage: "execute deletion", - }, - }, - }, - { - Name: "list", - Usage: "prints a list of all users", - Action: userList, + Usage: "deletes an existing user", + Action: usersDeleteAction, + ArgsUsage: "[username]", }, }, } -func userAdd(ctx *cli.Context) error { - return withDependencies(ctx, func(conf *config.Config) error { +func usersAddAction(ctx *cli.Context) error { + return callWithDependencies(ctx, func(conf *config.Config) error { uc := form.UserCreate{ UserName: strings.TrimSpace(ctx.String("username")), @@ -150,47 +145,58 @@ func userAdd(ctx *cli.Context) error { if err := entity.CreateWithPassword(uc); err != nil { return err } + return nil }) } -func userDelete(ctx *cli.Context) error { - return withDependencies(ctx, func(conf *config.Config) error { - username := ctx.Args()[0] - if !ctx.Bool("force") { - user := entity.FindUserByName(username) - if user != nil { - log.Infof("found user %s with uid: %s. Use -f to perform actual deletion\n", user.UserName, user.UserUID) - return nil +func usersDeleteAction(ctx *cli.Context) error { + return callWithDependencies(ctx, func(conf *config.Config) error { + userName := strings.TrimSpace(ctx.Args().First()) + + if userName == "" { + return errors.New("please provide a username") + } + + actionPrompt := promptui.Prompt{ + Label: fmt.Sprintf("Delete %s?", txt.Quote(userName)), + IsConfirm: true, + } + + if _, err := actionPrompt.Run(); err == nil { + if m := entity.FindUserByName(userName); m == nil { + return errors.New("user not found") + } else if err := m.Delete(); err != nil { + return err + } else { + log.Infof("%s deleted", txt.Quote(userName)) } - return errors.New("user not found") + } else { + log.Infof("keeping user") } - err := query.DeleteUserByName(username) - if err != nil { - log.Errorf("%s\n", err) - return nil - } - log.Infof("sucessfully deleted %s\n", username) + return nil }) } -func userList(ctx *cli.Context) error { - return withDependencies(ctx, func(conf *config.Config) error { - users := query.AllUsers() - fmt.Printf("%-16s %-16s %-16s\n", "Username", "Full Name", "Email") - fmt.Printf("%-16s %-16s %-16s\n", "--------", "---------", "-----") +func usersListAction(ctx *cli.Context) error { + return callWithDependencies(ctx, func(conf *config.Config) error { + users := query.RegisteredUsers() + log.Infof("found %d users", len(users)) + + fmt.Printf("%-4s %-16s %-16s %-16s\n", "ID", "LOGIN", "NAME", "EMAIL") + for _, user := range users { - fmt.Printf("%-16s %-16s %-16s", user.UserName, user.FullName, user.PrimaryEmail) + fmt.Printf("%-4d %-16s %-16s %-16s", user.ID, user.UserName, user.FullName, user.PrimaryEmail) fmt.Printf("\n") } - fmt.Printf("total users found: %v\n", len(users)) + return nil }) } -func userModify(ctx *cli.Context) error { - return withDependencies(ctx, func(conf *config.Config) error { +func usersModifyAction(ctx *cli.Context) error { + return callWithDependencies(ctx, func(conf *config.Config) error { username := ctx.Args().First() if username == "" { return errors.New("pass username as argument") @@ -231,16 +237,18 @@ func userModify(ctx *cli.Context) error { if err := u.Validate(); err != nil { return err } + if err := u.Save(); err != nil { return err } + fmt.Printf("user successfully updated: %v\n", u.UserName) return nil }) } -func withDependencies(ctx *cli.Context, f func(conf *config.Config) error) error { +func callWithDependencies(ctx *cli.Context, f func(conf *config.Config) error) error { conf := config.NewConfig(ctx) _, cancel := context.WithCancel(context.Background()) @@ -253,9 +261,10 @@ func withDependencies(ctx *cli.Context, f func(conf *config.Config) error) error conf.InitDb() defer conf.Shutdown() - // command is executed here + // Run command. if err := f(conf); err != nil { return err } + return nil } diff --git a/internal/config/flags.go b/internal/config/flags.go index 1be274b71..db88fcb7a 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -371,12 +371,12 @@ var GlobalFlags = []cli.Flag{ }, cli.StringFlag{ Name: "download-token", - Usage: "optional static `SECRET` url token for file downloads", + Usage: "static url `TOKEN` for original file downloads", EnvVar: "PHOTOPRISM_DOWNLOAD_TOKEN", }, cli.StringFlag{ Name: "preview-token", - Usage: "optional static `SECRET` url token for preview images and video streaming", + Usage: "static url `TOKEN` for thumbnails and video streaming", EnvVar: "PHOTOPRISM_PREVIEW_TOKEN", }, cli.StringFlag{ diff --git a/internal/entity/user.go b/internal/entity/user.go index 817dbc1b5..104b61ecc 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -195,6 +195,20 @@ func FindUserByUID(uid string) *User { } } +// Delete marks the entity as deleted. +func (m *User) Delete() error { + if m.ID <= 1 { + return fmt.Errorf("can't delete system user") + } + + return Db().Delete(m).Error +} + +// Deleted tests if the entity is marked as deleted. +func (m *User) Deleted() bool { + return m.DeletedAt != nil +} + // String returns an identifier that can be used in logs. func (m *User) String() string { if m.UserName != "" { diff --git a/internal/query/user.go b/internal/query/users.go similarity index 76% rename from internal/query/user.go rename to internal/query/users.go index a9b79c0ca..8631cae2d 100644 --- a/internal/query/user.go +++ b/internal/query/users.go @@ -22,12 +22,11 @@ func DeleteUserByName(userName string) error { return nil } -// AllUsers Returns a list of all registered Users. -func AllUsers() []entity.User { - var users []entity.User - if err := Db().Find(&users).Error; err != nil { - log.Error(err) - return []entity.User{} +// RegisteredUsers finds all registered users. +func RegisteredUsers() (result entity.Users) { + if err := Db().Where("id > 0").Find(&result).Error; err != nil { + log.Errorf("users: %s", err) } - return users + + return result } diff --git a/internal/query/user_test.go b/internal/query/users_test.go similarity index 72% rename from internal/query/user_test.go rename to internal/query/users_test.go index ebf4d06d8..9613e5e4b 100644 --- a/internal/query/user_test.go +++ b/internal/query/users_test.go @@ -30,13 +30,16 @@ func TestDeleteUserByName(t *testing.T) { }) } -func TestAllUsers(t *testing.T) { - t.Run("list all", func(t *testing.T) { - users := AllUsers() +func TestRegisteredUsers(t *testing.T) { + t.Run("success", func(t *testing.T) { + users := RegisteredUsers() + for _, user := range users { - log.Infof("user: %v, %s, %s, %s", user.ID, user.UserUID, user.UserName, user.FullName) + t.Logf("user: %v, %s, %s, %s", user.ID, user.UserUID, user.UserName, user.FullName) } - log.Infof("user count: %v", len(users)) - assert.Greater(t, len(users), 3) + + t.Logf("user count: %v", len(users)) + + assert.GreaterOrEqual(t, len(users), 3) }) }