package entity import ( "fmt" "net/mail" "path" "strings" "time" "github.com/jinzhu/gorm" "github.com/ulule/deepcopier" "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/list" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) // User identifier prefixes. const ( UserUID = byte('u') UserPrefix = "user" OwnerUnknown = "" UsernameLengthDefault = 1 PasswordLengthDefault = 8 ) // UsernameLength specifies the minimum length of the username in characters. var UsernameLength = UsernameLengthDefault // PasswordLength specifies the minimum length of a password in characters (runes, not bytes). var PasswordLength = PasswordLengthDefault // UsersPath is the relative path for user assets. var UsersPath = "users" // Users represents a list of users. type Users []User // User represents a person that may optionally log in as user. type User struct { ID int `gorm:"primary_key" json:"ID" yaml:"-"` UUID string `gorm:"type:VARBINARY(64);column:user_uuid;index;" json:"UUID,omitempty" yaml:"UUID,omitempty"` UserUID string `gorm:"type:VARBINARY(42);column:user_uid;unique_index;" json:"UID" yaml:"UID"` AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"` AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"` AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"` UserName string `gorm:"size:200;index;" json:"Name" yaml:"Name,omitempty"` DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"` UserEmail string `gorm:"size:255;index;" json:"Email" yaml:"Email,omitempty"` BackupEmail string `gorm:"size:255;" json:"BackupEmail,omitempty" yaml:"BackupEmail,omitempty"` UserRole string `gorm:"size:64;default:'';" json:"Role" yaml:"Role,omitempty"` UserAttr string `gorm:"size:1024;" json:"Attr" yaml:"Attr,omitempty"` SuperAdmin bool `json:"SuperAdmin" yaml:"SuperAdmin,omitempty"` CanLogin bool `json:"CanLogin" yaml:"CanLogin,omitempty"` LoginAt *time.Time `json:"LoginAt" yaml:"LoginAt,omitempty"` ExpiresAt *time.Time `sql:"index" json:"ExpiresAt,omitempty" yaml:"ExpiresAt,omitempty"` WebDAV bool `gorm:"column:webdav;" json:"WebDAV" yaml:"WebDAV,omitempty"` BasePath string `gorm:"type:VARBINARY(1024);" json:"BasePath" yaml:"BasePath,omitempty"` UploadPath string `gorm:"type:VARBINARY(1024);" json:"UploadPath" yaml:"UploadPath,omitempty"` CanInvite bool `json:"CanInvite" yaml:"CanInvite,omitempty"` InviteToken string `gorm:"type:VARBINARY(64);index;" json:"-" yaml:"-"` InvitedBy string `gorm:"size:64;" json:"-" yaml:"-"` VerifyToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"` VerifiedAt *time.Time `json:"VerifiedAt,omitempty" yaml:"VerifiedAt,omitempty"` ConsentAt *time.Time `json:"ConsentAt,omitempty" yaml:"ConsentAt,omitempty"` BornAt *time.Time `sql:"index" json:"BornAt,omitempty" yaml:"BornAt,omitempty"` UserDetails *UserDetails `gorm:"PRELOAD:true;foreignkey:UserUID;association_foreignkey:UserUID;" json:"Details,omitempty" yaml:"Details,omitempty"` UserSettings *UserSettings `gorm:"PRELOAD:true;foreignkey:UserUID;association_foreignkey:UserUID;" json:"Settings,omitempty" yaml:"Settings,omitempty"` UserShares UserShares `gorm:"-" json:"Shares,omitempty" yaml:"Shares,omitempty"` ResetToken string `gorm:"type:VARBINARY(64);" json:"-" yaml:"-"` PreviewToken string `gorm:"type:VARBINARY(64);column:preview_token;" json:"-" yaml:"-"` DownloadToken string `gorm:"type:VARBINARY(64);column:download_token;" json:"-" yaml:"-"` Thumb string `gorm:"type:VARBINARY(128);index;default:'';" json:"Thumb" yaml:"Thumb,omitempty"` ThumbSrc string `gorm:"type:VARBINARY(8);default:'';" json:"ThumbSrc" yaml:"ThumbSrc,omitempty"` RefID string `gorm:"type:VARBINARY(16);" json:"-" yaml:"-"` CreatedAt time.Time `json:"CreatedAt" yaml:"-"` UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"` DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"` } // TableName returns the entity table name. func (User) TableName() string { return "auth_users" } // NewUser creates a new user entity with defaults. func NewUser() (m *User) { uid := rnd.GenerateUID(UserUID) return &User{ UserUID: uid, UserDetails: NewUserDetails(uid), UserSettings: NewUserSettings(uid), PreviewToken: GenerateToken(), DownloadToken: GenerateToken(), RefID: rnd.RefID(UserPrefix), } } // LdapUser creates an LDAP user entity. func LdapUser(username, dn string) User { return User{ UserName: clean.Username(username), AuthID: dn, AuthProvider: authn.ProviderLDAP.String(), } } // FindUser returns the matching user or nil if it was not found. func FindUser(find User) *User { m := &User{} // Build query. stmt := UnscopedDb() if find.ID != 0 && find.UserName != "" { stmt = stmt.Where("id = ? OR user_name = ?", find.ID, find.UserName) } else if find.ID != 0 { stmt = stmt.Where("id = ?", find.ID) } else if rnd.IsUID(find.UserUID, UserUID) { stmt = stmt.Where("user_uid = ?", find.UserUID) } else if find.AuthProvider != "" && find.AuthID != "" && find.UserName != "" { stmt = stmt.Where("auth_provider = ? AND auth_id = ? OR user_name = ?", find.AuthProvider, find.AuthID, find.UserName) } else if find.UserName != "" { stmt = stmt.Where("user_name = ?", find.UserName) } else if find.UserEmail != "" { stmt = stmt.Where("user_email = ?", find.UserEmail) } else if find.AuthProvider != "" && find.AuthID != "" { stmt = stmt.Where("auth_provider = ? AND auth_id = ?", find.AuthProvider, find.AuthID) } else { return nil } // Find matching record. if err := stmt.First(m).Error; err != nil { return nil } // Fetch related records. return m.LoadRelated() } // FirstOrCreateUser returns an existing record, inserts a new record, or returns nil in case of an error. func FirstOrCreateUser(m *User) *User { if m == nil { return nil } if found := FindUser(*m); found != nil { return found } else if err := m.Create(); err != nil { event.AuditErr([]string{"user", "failed to create", "%s"}, err) return nil } else { return m } } // FindUserByName returns the matching user or nil if it was not found. func FindUserByName(userName string) *User { userName = clean.Username(userName) if userName == "" { return nil } return FindUser(User{UserName: userName}) } // FindLocalUser returns the matching local user or nil if it was not found. func FindLocalUser(userName string) *User { name := clean.Username(userName) if name == "" { return nil } m := &User{} providers := authn.LocalProviders // Build query. if err := UnscopedDb(). Where("user_name = ? AND auth_provider IN (?)", name, providers). First(m).Error; err != nil { return nil } // Return with related records. return m.LoadRelated() } // FindUserByUID returns the matching user or nil if it was not found. func FindUserByUID(uid string) *User { if rnd.InvalidUID(uid, UserUID) { return nil } return FindUser(User{UserUID: uid}) } // UID returns the unique id as string. func (m *User) UID() string { if m == nil { return "" } return m.UserUID } // SameUID checks if the given uid matches the own uid. func (m *User) SameUID(uid string) bool { if m == nil { return false } else if m.UserUID == "" || rnd.InvalidUID(uid, UserUID) { return false } return m.UserUID == uid } // InitAccount sets the name and password of the initial admin account. func (m *User) InitAccount(initName, initPasswd string) (updated bool) { // User must exist and the password must not be empty. initPasswd = strings.TrimSpace(initPasswd) if rnd.InvalidUID(m.UserUID, UserUID) || initPasswd == "" { return false } else if !m.CanLogIn() { log.Warnf("users: %s account is not allowed to log in", m.String()) } // Abort if user has a password. existingPasswd := FindPassword(m.UserUID) if existingPasswd != nil { return false } // Set initial password. initialPasswd := NewPassword(m.UserUID, initPasswd, true) // Save password. if err := initialPasswd.Save(); err != nil { event.AuditErr([]string{"user %s", "failed to change password", "%s"}, m.RefID, err) return false } // Change username if needed. if initName != "" && initName != m.UserName { if err := m.UpdateUsername(initName); err != nil { event.AuditErr([]string{"user %s", "failed to change username to %s", "%s"}, m.RefID, clean.Log(initName), err) } } return true } // Create new entity in the database. func (m *User) Create() (err error) { err = Db().Create(m).Error if err == nil { m.SaveRelated() } return err } // Save updates the record in the database or inserts a new record if it does not already exist. func (m *User) Save() (err error) { m.GenerateTokens(false) err = UnscopedDb().Save(m).Error if err == nil { m.SaveRelated() } return err } // Delete marks the entity as deleted. 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") } 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 } // IsDeleted checks if the user account has been deleted. func (m *User) IsDeleted() bool { if m.DeletedAt == nil { return false } return !m.DeletedAt.IsZero() } // LoadRelated loads related settings and details. func (m *User) LoadRelated() *User { m.Details() m.Settings() m.RefreshShares() return m } // SaveRelated saves related settings and details. func (m *User) SaveRelated() *User { if err := m.Settings().Save(); err != nil { event.AuditErr([]string{"user %s", "failed to save settings", "%s"}, m.RefID, err) } if err := m.Details().Save(); err != nil { event.AuditErr([]string{"user %s", "failed to save details", "%s"}, m.RefID, err) } return m } // Updates multiple properties in the database. func (m *User) Updates(values interface{}) error { return UnscopedDb().Model(m).Updates(values).Error } // BeforeCreate sets a random UID if needed before inserting a new row to the database. func (m *User) BeforeCreate(scope *gorm.Scope) error { if m.UserSettings != nil { m.UserSettings.UserUID = m.UserUID } if m.UserDetails != nil { m.UserDetails.UserUID = m.UserUID } m.GenerateTokens(false) if rnd.InvalidRefID(m.RefID) { m.RefID = rnd.RefID(UserPrefix) Log("user", "set ref id", scope.SetColumn("RefID", m.RefID)) } if rnd.IsUnique(m.UserUID, UserUID) { return nil } m.UserUID = rnd.GenerateUID(UserUID) return scope.SetColumn("UserUID", m.UserUID) } // IsExpired checks if the user account has expired. func (m *User) IsExpired() bool { if m.ExpiresAt == nil { return false } return m.ExpiresAt.Before(time.Now()) } // IsDisabled checks if the user account has been deleted or has expired. func (m *User) IsDisabled() bool { if m == nil { return true } return m.IsDeleted() || m.IsExpired() && !m.SuperAdmin } // UpdateLoginTime updates the login timestamp and returns it if successful. func (m *User) UpdateLoginTime() *time.Time { if m == nil { return nil } else if m.IsDeleted() { return nil } timeStamp := TimePointer() if err := Db().Model(m).UpdateColumn("LoginAt", timeStamp).Error; err != nil { return nil } m.LoginAt = timeStamp return timeStamp } // CanLogIn checks if the user is allowed to log in and use the web UI. func (m *User) CanLogIn() bool { if m == nil { return false } else if m.IsDeleted() || m.HasProvider(authn.ProviderNone) { return false } else if !m.CanLogin && !m.SuperAdmin || m.ID <= 0 || m.UserName == "" { return false } else if m.IsDisabled() || m.IsUnknown() || !m.IsRegistered() { return false } else { return acl.Resources.Allow(acl.ResourceConfig, m.AclRole(), acl.AccessOwn) } } // CanUseWebDAV checks whether the user is allowed to use WebDAV to synchronize files. func (m *User) CanUseWebDAV() bool { if m == nil { // Abort check if user is nil for any reason. return false } else if !m.WebDAV || m.ID <= 0 || m.IsDisabled() || m.IsUnknown() || !m.IsRegistered() || m.HasProvider(authn.ProviderNone) { // Deny WebDAV access if WebDAV is disabled, the user does not have a // regular, registered account, or the account has been deactivated. return false } else { // Check if the ACL allows downloading files via WebDAV based on the user role. return acl.Resources.Allow(acl.ResourceWebDAV, m.AclRole(), acl.ActionDownload) } } // CanUpload checks if the user is allowed to upload files. func (m *User) CanUpload() bool { if m == nil { // Abort check if user is nil for any reason. return false } else if m.IsDisabled() || m.HasProvider(authn.ProviderNone) || m.IsUnknown() { // Deny uploading if the user is unknown or the account has been deactivated. return false } else { // Check if the ACL allows uploading photos based on the user role. return acl.Resources.Allow(acl.ResourcePhotos, m.AclRole(), acl.ActionUpload) } } // DefaultBasePath returns the default base path of the user based on the user name. func (m *User) DefaultBasePath() string { if s := m.Handle(); s == "" { return "" } else { return path.Join(UsersPath, s) } } // GetBasePath returns the user's relative base path. func (m *User) GetBasePath() string { if m.BasePath == "" && m.HasRole("contributor") { m.BasePath = m.DefaultBasePath() } return m.BasePath } // SetBasePath changes the user's relative base path. func (m *User) SetBasePath(dir string) *User { if list.Contains(list.List{"", ".", "./", "/", "\\"}, dir) { m.BasePath = "" } else if dir == "~" && m.UserName != "" { m.BasePath = m.DefaultBasePath() } else { m.BasePath = clean.UserPath(dir) } return m } // GetUploadPath returns the user's relative upload path. func (m *User) GetUploadPath() string { basePath := m.GetBasePath() if list.Contains(list.List{"", ".", "./"}, m.UploadPath) { return basePath } else if basePath != "" && strings.HasPrefix(m.UploadPath, basePath+"/") { return m.UploadPath } else if basePath == "" && m.UploadPath == "~" && m.UserName != "" { return m.DefaultBasePath() } return path.Join(basePath, m.UploadPath) } // SetUploadPath changes the user's relative upload path. func (m *User) SetUploadPath(dir string) *User { basePath := m.GetBasePath() if list.Contains(list.List{"", ".", "./", "/", "\\"}, dir) { m.UploadPath = "" } else if basePath == "" && dir == "~" && m.UserName != "" { m.UploadPath = m.DefaultBasePath() } else { m.UploadPath = clean.UserPath(dir) } return m } // String returns an identifier that can be used in logs. func (m *User) String() string { if n := m.Username(); n != "" { return clean.LogQuote(n) } else if n = m.FullName(); n != "" { return clean.LogQuote(n) } return clean.Log(m.UserUID) } // Provider returns the authentication provider name. func (m *User) Provider() authn.ProviderType { if m.AuthProvider != "" { return authn.ProviderType(m.AuthProvider) } else if m.ID == Visitor.ID { return authn.ProviderLink } else if m.ID == 1 { return authn.ProviderLocal } else if m.UserName != "" && m.ID > 0 { return authn.ProviderDefault } return authn.ProviderNone } // HasProvider checks if the user has the given auth provider. func (m *User) HasProvider(t authn.ProviderType) bool { return t.String() == m.Provider().String() } // SetProvider set the authentication provider. func (m *User) SetProvider(t authn.ProviderType) *User { if m == nil { return nil } m.AuthProvider = t.String() return m } // Username returns the user's login name as sanitized string. func (m *User) Username() string { return clean.Username(m.UserName) } // SetUsername sets the login username to the specified string. func (m *User) SetUsername(login string) (err error) { if m.ID < 0 { return fmt.Errorf("system users cannot be modified") } login = clean.Username(login) // Empty? if login == "" { return fmt.Errorf("username is empty") } else if m.UserName == login { return nil } else if m.UserName != "" && m.ID != 1 { return fmt.Errorf("username cannot be changed") } // Update username and slug. m.UserName = login // Update display name. if m.DisplayName == "" || m.DisplayName == AdminDisplayName && m.ID == 1 { m.DisplayName = m.FullName() } return nil } // UpdateUsername changes the login name in the database. func (m *User) UpdateUsername(login string) (err error) { // Check if the name already exists or has not changed. if m.UserName == login || m.ID <= 0 { return nil } else if user := FindUserByName(login); user != nil { return fmt.Errorf("user %s already exists", clean.LogQuote(login)) } // Set new username. if err = m.SetUsername(login); err != nil { return err } // Save to database. return m.Updates(Map{ "UserName": m.UserName, "DisplayName": m.DisplayName, }) } // Email returns the user's login email for authentication. func (m *User) Email() string { return clean.Email(m.UserEmail) } // Handle returns the user's login handle. func (m *User) Handle() string { return clean.Handle(m.UserName) } // FullName returns the name of the user for display purposes. func (m *User) FullName() string { if m.DisplayName != "" { return m.DisplayName } if n := m.Details().DisplayName(); n != "" { return n } return clean.NameCapitalized(strings.ReplaceAll(m.Handle(), ".", " ")) } // SetRole sets the user role specified as string. func (m *User) SetRole(role string) *User { role = clean.Role(role) switch role { case "", "0", "false", "nil", "null", "nan": m.UserRole = acl.RoleNone.String() default: m.UserRole = acl.UserRoles[role].String() } return m } // HasRole checks the user role specified as string. func (m *User) HasRole(role acl.Role) bool { return m.AclRole() == role } // AclRole returns the user role for ACL permission checks. func (m *User) AclRole() acl.Role { role := clean.Role(m.UserRole) switch { case m.SuperAdmin: return acl.RoleAdmin case role == "": return acl.RoleNone case m.UserName == "": return acl.RoleVisitor default: return acl.UserRoles[role] } } // Settings returns the user settings and initializes them if necessary. func (m *User) Settings() *UserSettings { if m.UserSettings != nil { m.UserSettings.UserUID = m.UserUID return m.UserSettings } else if m.UID() == "" { m.UserSettings = &UserSettings{} return m.UserSettings } else if err := CreateUserSettings(m); err != nil { m.UserSettings = NewUserSettings(m.UserUID) } return m.UserSettings } // Details returns user profile information and initializes it if needed. func (m *User) Details() *UserDetails { if m.UserDetails != nil { m.UserDetails.UserUID = m.UserUID return m.UserDetails } else if m.UID() == "" { m.UserDetails = &UserDetails{} return m.UserDetails } else if err := CreateUserDetails(m); err != nil { m.UserDetails = NewUserDetails(m.UserUID) } return m.UserDetails } // Attr returns optional user account attributes as sanitized string. // Example: https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties func (m *User) Attr() string { return clean.Attr(m.UserAttr) } // IsRegistered checks if this user has a registered account with a valid ID, username, and role. func (m *User) IsRegistered() bool { if m == nil { return false } // Registered users must have an ID, a UID, a username and a known role, except visitor. return m.ID > 0 && m.UserName != "" && rnd.IsUID(m.UserUID, UserUID) && !m.IsVisitor() } // NotRegistered checks if the user is not registered with an own account. func (m *User) NotRegistered() bool { return !m.IsRegistered() } // Equal returns true if the user specified matches. func (m *User) Equal(u *User) bool { if m == nil || u == nil { return false } return m.UserUID == u.UserUID } // IsAdmin checks if the user is an admin with username. func (m *User) IsAdmin() bool { if m == nil { return false } return m.IsSuperAdmin() || m.IsRegistered() && m.AclRole() == acl.RoleAdmin } // IsSuperAdmin checks if the user is a super admin. func (m *User) IsSuperAdmin() bool { if m == nil { return false } return m.SuperAdmin } // IsVisitor checks if the user is a sharing link visitor. func (m *User) IsVisitor() bool { return m.AclRole() == acl.RoleVisitor || m.ID == Visitor.ID } // HasSharedAccessOnly checks if the user as only access to shared resources. func (m *User) HasSharedAccessOnly(resource acl.Resource) bool { if acl.Resources.Deny(resource, m.AclRole(), acl.AccessShared) { return false } return acl.Resources.DenyAll(resource, m.AclRole(), acl.Permissions{acl.AccessAll, acl.AccessLibrary}) } // IsUnknown checks if the user is unknown. func (m *User) IsUnknown() bool { if m == nil { return true } return !rnd.IsUID(m.UserUID, UserUID) || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID || m.HasRole(acl.RoleNone) } // DeleteSessions deletes all active user sessions except those passed as argument. func (m *User) DeleteSessions(omit []string) (deleted int) { if m.UserUID == "" { return 0 } // Compose update statement. stmt := Db() // Find all user sessions except the session ids passed as argument. if len(omit) == 0 { stmt = stmt.Where("user_uid = ?", m.UserUID) } else { stmt = stmt.Where("user_uid = ? AND id NOT IN (?)", m.UserUID, omit) } // Exclude client access tokens. stmt = stmt.Where("auth_provider NOT IN (?)", authn.ClientProviders) // Fetch sessions from database. sess := Sessions{} if err := stmt.Find(&sess).Error; err != nil { event.AuditErr([]string{"user %s", "failed to invalidate sessions", "%s"}, m.RefID, err) return 0 } // Delete sessions from cache and database. for _, s := range sess { if err := s.Delete(); err != nil { event.AuditWarn([]string{"user %s", "failed to invalidate session %s", "%s"}, m.RefID, clean.Log(s.RefID), err) } else { deleted++ } } // Return number of deleted sessions. return deleted } // SetPassword sets a new password stored as hash. func (m *User) SetPassword(password string) error { if !m.IsRegistered() { return fmt.Errorf("only registered users can change their password") } if len([]rune(password)) < PasswordLength { return fmt.Errorf("password must have at least %d characters", PasswordLength) } else if len(password) > txt.ClipPassword { return fmt.Errorf("password must have less than %d characters", txt.ClipPassword) } pw := NewPassword(m.UserUID, password, false) if err := pw.Save(); err != nil { return err } return m.RegenerateTokens() } // HasPassword checks if the user has the specified password and the account is registered. func (m *User) HasPassword(s string) bool { return !m.WrongPassword(s) } // WrongPassword checks if the given password is incorrect or the account is not registered. func (m *User) WrongPassword(s string) bool { // Registered user? if !m.IsRegistered() { log.Warn("only registered users can log in") return true } // Empty password? if s == "" { return true } // Fetch password. pw := FindPassword(m.UserUID) // Found? if pw == nil { return true } // Invalid? if pw.IsWrong(s) { return true } return false } // Validate checks if username, email and role are valid and returns an error otherwise. func (m *User) Validate() (err error) { // Validate username. if userName, nameErr := authn.Username(m.UserName); nameErr != nil { return fmt.Errorf("username is %s", nameErr.Error()) } else { m.UserName = userName } // Check if username also meets the length requirements. if len(m.Username()) < UsernameLength { return fmt.Errorf("username must have at least %d characters", UsernameLength) } // Check user role. if acl.UserRoles[m.UserRole] == "" { return fmt.Errorf("user role %s is invalid", clean.LogQuote(m.UserRole)) } // Check if the username is unique. var duplicate = User{} if err = Db(). Where("user_name = ? AND id <> ?", m.UserName, m.ID). First(&duplicate).Error; err == nil { return fmt.Errorf("user %s already exists", clean.LogQuote(m.UserName)) } else if err != gorm.ErrRecordNotFound { return err } // Skip email check? if m.UserEmail == "" { return nil } // Parse and validate email address. if a, err := mail.ParseAddress(m.UserEmail); err != nil { return fmt.Errorf("email %s is invalid", clean.LogQuote(m.UserEmail)) } else if email := a.Address; !strings.ContainsRune(email, '.') { return fmt.Errorf("email %s does not have a fully qualified domain", clean.LogQuote(m.UserEmail)) } else { m.UserEmail = email } // Check if the email is unique. if err = Db(). Where("user_email = ? AND id <> ?", m.UserEmail, m.ID). First(&duplicate).Error; err == nil { return fmt.Errorf("email %s already exists", clean.Log(m.UserEmail)) } else if err != gorm.ErrRecordNotFound { return err } return nil } // SetFormValues sets the values specified in the form. func (m *User) SetFormValues(frm form.User) *User { m.UserName = frm.Username() m.SetProvider(frm.Provider()) m.UserEmail = frm.Email() m.DisplayName = frm.DisplayName m.SuperAdmin = frm.SuperAdmin m.CanLogin = frm.CanLogin m.WebDAV = frm.WebDAV m.SetRole(frm.Role()) m.UserAttr = frm.Attr() m.SetBasePath(frm.BasePath) m.SetUploadPath(frm.UploadPath) // Set display name default if empty. if m.DisplayName == "" || m.DisplayName == AdminDisplayName && m.ID == 1 { m.DisplayName = m.FullName() } return m } // GenerateTokens generates preview and download tokens as needed. func (m *User) GenerateTokens(force bool) *User { if m.ID < 0 { return m } if m.PreviewToken == "" || force { m.PreviewToken = GenerateToken() } if m.DownloadToken == "" || force { m.DownloadToken = GenerateToken() } return m } // RegenerateTokens replaces the existing preview and download tokens. func (m *User) RegenerateTokens() error { if m.ID < 0 { return nil } m.GenerateTokens(true) return m.Updates(Map{"PreviewToken": m.PreviewToken, "DownloadToken": m.DownloadToken}) } // RefreshShares updates the list of shares. func (m *User) RefreshShares() *User { m.UserShares = FindUserShares(m.UID()) return m } // NoShares checks if the user has no shares yet. func (m *User) NoShares() bool { if m.NotRegistered() { return true } return m.UserShares.Empty() } // HasShares checks if the user has any shares. func (m *User) HasShares() bool { return !m.NoShares() } // HasShare if a uid was shared with the user. func (m *User) HasShare(uid string) bool { if m.NotRegistered() || m.NoShares() { return false } // Check if the share list contains the specified UID. return m.UserShares.Contains(uid) } // SharedUIDs returns shared entity UIDs. func (m *User) SharedUIDs() UIDs { if m.IsRegistered() && m.NoShares() { m.RefreshShares() } return m.UserShares.UIDs() } // RedeemToken updates shared entity UIDs using the specified token. func (m *User) RedeemToken(token string) (n int) { if !m.IsRegistered() { return 0 } // Find links. links := FindValidLinks(token, "") // Found? if n = len(links); n == 0 { return n } // Find shares. for _, link := range links { if found := FindUserShare(UserShare{UserUID: m.UID(), ShareUID: link.ShareUID}); found == nil { share := NewUserShare(m.UID(), link.ShareUID, link.Perm, link.ExpiresAt()) share.LinkUID = link.LinkUID share.Comment = link.Comment if err := share.Save(); err != nil { event.AuditErr([]string{"user %s", "token %s", "failed to redeem shares", "%s"}, m.RefID, clean.Log(token), err) } else { link.Redeem() } } else if err := found.UpdateLink(link); err != nil { event.AuditErr([]string{"user %s", "token %s", "failed to update shares", "%s"}, m.RefID, clean.Log(token), err) } } return n } // Form returns a populated user form to perform changes. func (m *User) Form() (form.User, error) { frm := form.User{UserDetails: &form.UserDetails{}} if err := deepcopier.Copy(m).To(&frm); err != nil { return frm, err } if err := deepcopier.Copy(m.UserDetails).To(frm.UserDetails); err != nil { return frm, err } return frm, nil } // PrivilegeLevelChange checks if saving the form changes the user privileges. func (m *User) PrivilegeLevelChange(f form.User) bool { return m.UserRole != f.Role() || m.SuperAdmin != f.SuperAdmin || m.CanLogin != f.CanLogin || m.WebDAV != f.WebDAV || m.UserAttr != f.Attr() || m.AuthProvider != f.Provider().String() || m.BasePath != f.BasePath || m.UploadPath != f.UploadPath } // SaveForm updates the entity using form data and stores it in the database. func (m *User) SaveForm(f form.User, changePrivileges bool) error { if m.UserName == "" || m.ID <= 0 { return fmt.Errorf("system users cannot be modified") } else if (m.ID == 1 || f.SuperAdmin) && acl.RoleAdmin.NotEqual(f.Role()) { return fmt.Errorf("super admin must not have a non-admin role") } else if f.BasePath != "" && clean.UserPath(f.BasePath) == "" { return fmt.Errorf("invalid base folder") } else if f.UploadPath != "" && clean.UserPath(f.UploadPath) == "" { return fmt.Errorf("invalid upload folder") } // Ignore details if not set. if f.UserDetails == nil { // Ignore. } else if err := deepcopier.Copy(f.UserDetails).To(m.UserDetails); err != nil { return err } else { m.UserDetails.UserAbout = txt.Clip(m.UserDetails.UserAbout, txt.ClipComment) m.UserDetails.UserBio = txt.Clip(m.UserDetails.UserBio, txt.ClipText) } // Sanitize display name. if n := clean.Name(f.DisplayName); n != "" && n != m.DisplayName { m.SetDisplayName(n, SrcManual) } // Set display name default if empty. if m.DisplayName == "" || m.DisplayName == AdminDisplayName && m.ID == 1 { m.DisplayName = m.FullName() } // Sanitize email address. if email := f.Email(); email != "" && email != m.UserEmail { m.UserEmail = email m.VerifiedAt = nil m.VerifyToken = GenerateToken() } // Change user privileges only if allowed. if changePrivileges { m.SetRole(f.Role()) m.SuperAdmin = f.SuperAdmin m.CanLogin = f.CanLogin m.WebDAV = f.WebDAV m.UserAttr = f.Attr() m.SetProvider(f.Provider()) m.SetBasePath(f.BasePath) m.SetUploadPath(f.UploadPath) } // Ensure super admins never have a non-admin role. if m.SuperAdmin { m.SetRole(acl.RoleAdmin.String()) } // Make sure that the initial admin user cannot lock itself out. if m.ID == Admin.ID && (m.AclRole() != acl.RoleAdmin || !m.SuperAdmin || !m.CanLogin) { m.SetRole(acl.RoleAdmin.String()) m.SuperAdmin = true m.CanLogin = true } return m.Save() } // SetDisplayName sets a new display name and, if possible, splits it into its components. func (m *User) SetDisplayName(name, src string) *User { name = clean.Name(name) d := m.Details() priority := SrcPriority[src] >= SrcPriority[d.NameSrc] if name == "" || !priority && m.DisplayName != "" { return m } m.DisplayName = name if !priority { return m } d.NameSrc = src // Try to parse name into components. n := txt.ParseName(name) d.NameTitle = n.Title d.GivenName = n.Given d.MiddleName = n.Middle d.FamilyName = n.Family d.NameSuffix = n.Suffix d.NickName = n.Nick return m } // SetGivenName updates the user's given name. func (m *User) SetGivenName(name string) *User { m.Details().SetGivenName(name) return m } // SetFamilyName updates the user's family name. func (m *User) SetFamilyName(name string) *User { m.Details().SetFamilyName(name) return m } // SetAvatar updates the user avatar image. func (m *User) SetAvatar(thumb, thumbSrc string) error { if m.UserName == "" || m.ID <= 0 { return fmt.Errorf("system user avatars cannot be changed") } if SrcPriority[thumbSrc] < SrcPriority[m.ThumbSrc] && m.Thumb != "" { return fmt.Errorf("no permission to change avatar") } m.Thumb = thumb m.ThumbSrc = thumbSrc return m.Updates(Map{"Thumb": m.Thumb, "ThumbSrc": m.ThumbSrc}) }