CLI: Restore user accounts on demand #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
5f808cddb4
commit
7c63a86f80
8 changed files with 115 additions and 27 deletions
|
@ -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)
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +17,13 @@ var UsersRemoveCommand = cli.Command{
|
|||
Name: "rm",
|
||||
Usage: "Removes a user account",
|
||||
ArgsUsage: "[username]",
|
||||
Action: usersRemoveAction,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "don't ask for confirmation",
|
||||
},
|
||||
},
|
||||
Action: usersRemoveAction,
|
||||
}
|
||||
|
||||
// usersRemoveAction deletes a user account.
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
actionPrompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf("Remove user %s?", m.String()),
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
if _, err := actionPrompt.Run(); err == nil {
|
||||
if err = m.Delete(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
log.Infof("user %s has been removed", m.String())
|
||||
if !ctx.Bool("force") {
|
||||
actionPrompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf("Delete user %s?", m.String()),
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
if _, err := actionPrompt.Run(); err != nil {
|
||||
log.Infof("user %s was not deleted", m.String())
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
log.Infof("user %s was not removed", m.String())
|
||||
}
|
||||
|
||||
if err := m.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("user %s has been deleted", m.String())
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue