CLI: Restore user accounts on demand #98

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-02-20 20:24:04 +01:00
parent 5f808cddb4
commit 7c63a86f80
8 changed files with 115 additions and 27 deletions

View file

@ -57,6 +57,8 @@ func passwdAction(ctx *cli.Context) error {
if m == nil {
return fmt.Errorf("user %s not found", clean.LogQuote(id))
} else if m.Deleted() {
return fmt.Errorf("user %s has been deleted", clean.LogQuote(id))
}
log.Infof("please enter a new password for %s (minimum %d characters)\n", clean.Log(m.Name()), entity.PasswordLength)

View file

@ -47,7 +47,33 @@ func usersAddAction(ctx *cli.Context) error {
return err
}
frm.UserName = clean.Username(res)
frm.UserName = clean.DN(res)
}
// Check if account exists but is deleted.
if frm.UserName == "" {
return fmt.Errorf("username is required")
} else if m := entity.FindUserByName(frm.UserName); m != nil {
if !m.Deleted() {
return fmt.Errorf("user already exists")
}
prompt := promptui.Prompt{
Label: fmt.Sprintf("Restore user %s?", m.String()),
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("user already exists")
}
if err := m.RestoreFromCli(ctx, frm.Password); err != nil {
return err
}
log.Infof("user %s has been restored", m.String())
return nil
}
if interactive && frm.UserEmail == "" {

View file

@ -3,13 +3,13 @@ package commands
import (
"fmt"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/manifoldco/promptui"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
)
// UsersModCommand configures the command name, flags, and action.
@ -46,6 +46,21 @@ func usersModAction(ctx *cli.Context) error {
return fmt.Errorf("user %s not found", clean.LogQuote(id))
}
// Check if account exists but is deleted.
if m.Deleted() {
prompt := promptui.Prompt{
Label: fmt.Sprintf("Restore user %s?", m.String()),
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("user already exists")
}
m.DeletedAt = nil
log.Infof("user %s will be restored", m.String())
}
// Set values.
if err := m.SetValuesFromCli(ctx); err != nil {
return err

View file

@ -3,14 +3,13 @@ package commands
import (
"fmt"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/manifoldco/promptui"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
)
// UsersRemoveCommand configures the command name, flags, and action.
@ -18,6 +17,12 @@ var UsersRemoveCommand = cli.Command{
Name: "rm",
Usage: "Removes a user account",
ArgsUsage: "[username]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "force, f",
Usage: "don't ask for confirmation",
},
},
Action: usersRemoveAction,
}
@ -44,23 +49,28 @@ func usersRemoveAction(ctx *cli.Context) error {
if m == nil {
return fmt.Errorf("user %s not found", clean.LogQuote(id))
} else if m.Deleted() {
return fmt.Errorf("user %s has already been deleted", clean.LogQuote(id))
}
if !ctx.Bool("force") {
actionPrompt := promptui.Prompt{
Label: fmt.Sprintf("Remove user %s?", m.String()),
Label: fmt.Sprintf("Delete user %s?", m.String()),
IsConfirm: true,
}
if _, err := actionPrompt.Run(); err == nil {
if err = m.Delete(); err != nil {
if _, err := actionPrompt.Run(); err != nil {
log.Infof("user %s was not deleted", m.String())
return nil
}
}
if err := m.Delete(); err != nil {
return err
} else {
log.Infof("user %s has been removed", m.String())
}
} else {
log.Infof("user %s was not removed", m.String())
}
log.Infof("user %s has been deleted", m.String())
return nil
})
}

View file

@ -232,7 +232,7 @@ func (m *User) Create() (err error) {
func (m *User) Save() (err error) {
m.GenerateTokens(false)
err = Db().Save(m).Error
err = UnscopedDb().Save(m).Error
if err == nil {
m.SaveRelated()
@ -242,12 +242,22 @@ func (m *User) Save() (err error) {
}
// Delete marks the entity as deleted.
func (m *User) Delete() error {
func (m *User) Delete() (err error) {
if m.ID <= 1 {
return fmt.Errorf("cannot delete system user")
} else if m.UserUID == "" {
return fmt.Errorf("uid is required to delete user")
}
return Db().Delete(m).Error
if err = UnscopedDb().Delete(Session{}, "user_uid = ?", m.UserUID).Error; err != nil {
event.AuditErr([]string{"user %s", "delete", "failed to remove sessions", "%s"}, m.RefID, err)
}
err = Db().Delete(m).Error
FlushSessionCache()
return err
}
// Deleted checks if the user account has been deleted.
@ -325,7 +335,11 @@ func (m *User) Disabled() bool {
// CanLogIn checks if the user is allowed to log in and use the web UI.
func (m *User) CanLogIn() bool {
if !m.CanLogin && !m.SuperAdmin || m.ID <= 0 || m.UserName == "" {
if m == nil {
return false
} else if m.Deleted() {
return false
} else if !m.CanLogin && !m.SuperAdmin || m.ID <= 0 || m.UserName == "" {
return false
} else if role := m.AclRole(); m.Disabled() || role == acl.RoleUnknown {
return false
@ -404,9 +418,9 @@ func (m *User) SetUploadPath(dir string) *User {
// String returns an identifier that can be used in logs.
func (m *User) String() string {
if n := m.Name(); n != "" {
return clean.Log(n)
return clean.LogQuote(n)
} else if n = m.FullName(); n != "" {
return clean.Log(n)
return clean.LogQuote(n)
}
return clean.Log(m.UserUID)

View file

@ -58,3 +58,24 @@ func (m *User) SetValuesFromCli(ctx *cli.Context) error {
return m.Validate()
}
// RestoreFromCli restored the account from a CLI context.
func (m *User) RestoreFromCli(ctx *cli.Context, newPassword string) (err error) {
m.DeletedAt = nil
// Set values.
if err = m.SetValuesFromCli(ctx); err != nil {
return err
}
// Save values.
if err = m.Save(); err != nil {
return err
} else if newPassword == "" {
return nil
} else if err = m.SetPassword(newPassword); err != nil {
return err
}
return nil
}

View file

@ -384,11 +384,11 @@ func TestUser_String(t *testing.T) {
})
t.Run("FullName", func(t *testing.T) {
p := User{UserUID: "abc123", UserName: "", DisplayName: "Test"}
assert.Equal(t, "Test", p.String())
assert.Equal(t, "'Test'", p.String())
})
t.Run("UserName", func(t *testing.T) {
p := User{UserUID: "abc123", UserName: "Super-User ", DisplayName: "Test"}
assert.Equal(t, "super-user", p.String())
assert.Equal(t, "'super-user'", p.String())
})
}

View file

@ -76,7 +76,7 @@ func (m *Migrations) Start(db *gorm.DB, opt Options) {
// Log information about existing migrations.
if prev := len(executed); prev > 0 {
stage := fmt.Sprintf("previously executed %s stage", opt.StageName())
log.Debugf("migrate: found %s", english.Plural(len(executed), stage+" migration", stage+" migrations"))
log.Tracef("migrate: found %s", english.Plural(len(executed), stage+" migration", stage+" migrations"))
}
// Run migrations.