Auth: Refactor user management commands #98

This commit is contained in:
Michael Mayer 2021-08-20 00:10:26 +02:00
parent 5cec098524
commit fa8e02b430
7 changed files with 92 additions and 67 deletions

View file

@ -70,10 +70,10 @@ func main() {
commands.RestoreCommand, commands.RestoreCommand,
commands.ResetCommand, commands.ResetCommand,
commands.ConfigCommand, commands.ConfigCommand,
commands.UsersCommand,
commands.PasswdCommand, commands.PasswdCommand,
commands.VersionCommand, commands.VersionCommand,
commands.StatusCommand, commands.StatusCommand,
commands.UserCommand,
} }
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {

View file

@ -14,7 +14,7 @@ import (
// FacesCommand registers the faces cli command. // FacesCommand registers the faces cli command.
var FacesCommand = cli.Command{ var FacesCommand = cli.Command{
Name: "faces", Name: "faces",
Usage: "Runs facial recognition commands", Usage: "Facial recognition sub-commands",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{ {
Name: "stats", Name: "stats",

View file

@ -8,6 +8,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/manifoldco/promptui"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
@ -16,15 +17,20 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// UserCommand Create, List, Update and Delete Users. // UsersCommand registers user management commands.
var UserCommand = cli.Command{ var UsersCommand = cli.Command{
Name: "users", Name: "users",
Usage: "Manage Users from CLI", Usage: "User management sub-commands",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{
Name: "list",
Usage: "lists registered users",
Action: usersListAction,
},
{ {
Name: "add", Name: "add",
Usage: "creates a new user. Provide at least username and password", Usage: "adds a new user",
Action: userAdd, Action: usersAddAction,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "fullname, n", Name: "fullname, n",
@ -45,9 +51,9 @@ var UserCommand = cli.Command{
}, },
}, },
{ {
Name: "modify", Name: "update",
Usage: "modify a users information.", Usage: "updates user information",
Action: userModify, Action: usersModifyAction,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "fullname, n", Name: "fullname, n",
@ -69,26 +75,15 @@ var UserCommand = cli.Command{
}, },
{ {
Name: "delete", Name: "delete",
Usage: "deletes user by username", Usage: "deletes an existing user",
Action: userDelete, Action: usersDeleteAction,
ArgsUsage: "takes username as argument", ArgsUsage: "[username]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "force, f",
Usage: "execute deletion",
},
},
},
{
Name: "list",
Usage: "prints a list of all users",
Action: userList,
}, },
}, },
} }
func userAdd(ctx *cli.Context) error { func usersAddAction(ctx *cli.Context) error {
return withDependencies(ctx, func(conf *config.Config) error { return callWithDependencies(ctx, func(conf *config.Config) error {
uc := form.UserCreate{ uc := form.UserCreate{
UserName: strings.TrimSpace(ctx.String("username")), UserName: strings.TrimSpace(ctx.String("username")),
@ -150,47 +145,58 @@ func userAdd(ctx *cli.Context) error {
if err := entity.CreateWithPassword(uc); err != nil { if err := entity.CreateWithPassword(uc); err != nil {
return err return err
} }
return nil return nil
}) })
} }
func userDelete(ctx *cli.Context) error { func usersDeleteAction(ctx *cli.Context) error {
return withDependencies(ctx, func(conf *config.Config) error { return callWithDependencies(ctx, func(conf *config.Config) error {
username := ctx.Args()[0] userName := strings.TrimSpace(ctx.Args().First())
if !ctx.Bool("force") {
user := entity.FindUserByName(username) if userName == "" {
if user != nil { return errors.New("please provide a username")
log.Infof("found user %s with uid: %s. Use -f to perform actual deletion\n", user.UserName, user.UserUID)
return nil
} }
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") return errors.New("user not found")
} else if err := m.Delete(); err != nil {
return err
} else {
log.Infof("%s deleted", txt.Quote(userName))
} }
err := query.DeleteUserByName(username) } else {
if err != nil { log.Infof("keeping user")
log.Errorf("%s\n", err)
return nil
} }
log.Infof("sucessfully deleted %s\n", username)
return nil return nil
}) })
} }
func userList(ctx *cli.Context) error { func usersListAction(ctx *cli.Context) error {
return withDependencies(ctx, func(conf *config.Config) error { return callWithDependencies(ctx, func(conf *config.Config) error {
users := query.AllUsers() users := query.RegisteredUsers()
fmt.Printf("%-16s %-16s %-16s\n", "Username", "Full Name", "Email") log.Infof("found %d users", len(users))
fmt.Printf("%-16s %-16s %-16s\n", "--------", "---------", "-----")
fmt.Printf("%-4s %-16s %-16s %-16s\n", "ID", "LOGIN", "NAME", "EMAIL")
for _, user := range users { 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("\n")
} }
fmt.Printf("total users found: %v\n", len(users))
return nil return nil
}) })
} }
func userModify(ctx *cli.Context) error { func usersModifyAction(ctx *cli.Context) error {
return withDependencies(ctx, func(conf *config.Config) error { return callWithDependencies(ctx, func(conf *config.Config) error {
username := ctx.Args().First() username := ctx.Args().First()
if username == "" { if username == "" {
return errors.New("pass username as argument") return errors.New("pass username as argument")
@ -231,16 +237,18 @@ func userModify(ctx *cli.Context) error {
if err := u.Validate(); err != nil { if err := u.Validate(); err != nil {
return err return err
} }
if err := u.Save(); err != nil { if err := u.Save(); err != nil {
return err return err
} }
fmt.Printf("user successfully updated: %v\n", u.UserName) fmt.Printf("user successfully updated: %v\n", u.UserName)
return nil 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) conf := config.NewConfig(ctx)
_, cancel := context.WithCancel(context.Background()) _, cancel := context.WithCancel(context.Background())
@ -253,9 +261,10 @@ func withDependencies(ctx *cli.Context, f func(conf *config.Config) error) error
conf.InitDb() conf.InitDb()
defer conf.Shutdown() defer conf.Shutdown()
// command is executed here // Run command.
if err := f(conf); err != nil { if err := f(conf); err != nil {
return err return err
} }
return nil return nil
} }

View file

@ -371,12 +371,12 @@ var GlobalFlags = []cli.Flag{
}, },
cli.StringFlag{ cli.StringFlag{
Name: "download-token", Name: "download-token",
Usage: "optional static `SECRET` url token for file downloads", Usage: "static url `TOKEN` for original file downloads",
EnvVar: "PHOTOPRISM_DOWNLOAD_TOKEN", EnvVar: "PHOTOPRISM_DOWNLOAD_TOKEN",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "preview-token", 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", EnvVar: "PHOTOPRISM_PREVIEW_TOKEN",
}, },
cli.StringFlag{ cli.StringFlag{

View file

@ -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. // String returns an identifier that can be used in logs.
func (m *User) String() string { func (m *User) String() string {
if m.UserName != "" { if m.UserName != "" {

View file

@ -22,12 +22,11 @@ func DeleteUserByName(userName string) error {
return nil return nil
} }
// AllUsers Returns a list of all registered Users. // RegisteredUsers finds all registered users.
func AllUsers() []entity.User { func RegisteredUsers() (result entity.Users) {
var users []entity.User if err := Db().Where("id > 0").Find(&result).Error; err != nil {
if err := Db().Find(&users).Error; err != nil { log.Errorf("users: %s", err)
log.Error(err)
return []entity.User{}
} }
return users
return result
} }

View file

@ -30,13 +30,16 @@ func TestDeleteUserByName(t *testing.T) {
}) })
} }
func TestAllUsers(t *testing.T) { func TestRegisteredUsers(t *testing.T) {
t.Run("list all", func(t *testing.T) { t.Run("success", func(t *testing.T) {
users := AllUsers() users := RegisteredUsers()
for _, user := range users { 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)
}) })
} }