package service import ( "context" "fmt" "io" "mime/multipart" "net/mail" "regexp" "strconv" "strings" "time" "unicode" "github.com/cortezaproject/corteza/server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza/server/pkg/auth" "github.com/cortezaproject/corteza/server/pkg/errors" "github.com/cortezaproject/corteza/server/pkg/eventbus" "github.com/cortezaproject/corteza/server/pkg/filter" "github.com/cortezaproject/corteza/server/pkg/handle" "github.com/cortezaproject/corteza/server/pkg/id" "github.com/cortezaproject/corteza/server/pkg/label" "github.com/cortezaproject/corteza/server/pkg/rbac" "github.com/cortezaproject/corteza/server/pkg/sass" "github.com/cortezaproject/corteza/server/store" "github.com/cortezaproject/corteza/server/system/service/event" "github.com/cortezaproject/corteza/server/system/types" ) const ( maskPrivateDataEmail = "####.#######@######.###" maskPrivateDataName = "##### ##########" ) type ( user struct { actionlog actionlog.Recorder settings *types.AppSettings userGroups ugMemberManager auth userAuth ac userAccessController eventbus eventDispatcher store store.Storer opt UserOptions // List (cache) of preloaded users, accessible by handle // // It also does negative caching by assigning empty User structs preloaded map[string]*types.User att AttachmentService } ugMemberManager interface { AssignGroupMembers(group id.ID, members ...id.ID) (err error) RemoveGroupMembers(group id.ID, members ...id.ID) (err error) } synteticUserDataGen interface { Name() string Username() string Number(int, int) int } UserOptions struct { LimitUsers int } userAuth interface { CheckPasswordStrength(string) bool SetPasswordCredentials(context.Context, uint64, string) error RemovePasswordCredentials(context.Context, uint64) error RemoveAccessTokens(context.Context, *types.User) error } userAccessController interface { CanSearchUsers(context.Context) bool CanCreateUser(context.Context) bool CanReadUser(context.Context, *types.User) bool CanUpdateUser(context.Context, *types.User) bool CanDeleteUser(context.Context, *types.User) bool CanSuspendUser(context.Context, *types.User) bool CanUnsuspendUser(context.Context, *types.User) bool CanUnmaskEmailOnUser(context.Context, *types.User) bool CanUnmaskNameOnUser(context.Context, *types.User) bool } UserService interface { FindByEmail(ctx context.Context, email string) (*types.User, error) FindByHandle(ctx context.Context, handle string) (*types.User, error) FindByID(ctx context.Context, id uint64) (*types.User, error) FindByAny(ctx context.Context, identifier interface{}) (*types.User, error) Find(context.Context, types.UserFilter) (types.UserSet, types.UserFilter, error) Create(ctx context.Context, input *types.User) (*types.User, error) Update(ctx context.Context, mod *types.User) (*types.User, error) ToggleEmailConfirmation(ctx context.Context, userID uint64, confirm bool) error CreateWithAvatar(ctx context.Context, input *types.User, avatar io.Reader) (*types.User, error) UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (*types.User, error) Delete(ctx context.Context, id uint64) error Suspend(ctx context.Context, id uint64) error Unsuspend(ctx context.Context, id uint64) error Undelete(ctx context.Context, id uint64) error SetPassword(ctx context.Context, userID uint64, password string) error DeleteAuthTokensByUserID(ctx context.Context, userID uint64) (err error) DeleteAuthSessionsByUserID(ctx context.Context, userID uint64) (err error) UploadAvatar(ctx context.Context, userID uint64, Upload *multipart.FileHeader) (err error) GenerateAvatar(ctx context.Context, userID uint64, bgColor string, initialColor string) (err error) DeleteAvatar(ctx context.Context, id uint64) error } ) func User(opt UserOptions) *user { return &user{ eventbus: eventbus.Service(), ac: DefaultAccessControl, settings: CurrentSettings, auth: DefaultAuth, userGroups: rbac.Global(), store: DefaultStore, actionlog: DefaultActionlog, opt: opt, preloaded: make(map[string]*types.User), att: DefaultAttachment, } } func (svc user) FindByID(ctx context.Context, userID uint64) (u *types.User, err error) { var ( uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() error { u, err = loadUser(ctx, svc.store, userID) if u, err = svc.proc(ctx, u, err); err != nil { return err } uaProps.setUser(u) // If profile avatar settings is enabled and a user doesn't have an avatar image, // generate one automatically when fetching their user information. if svc.settings.Auth.Internal.ProfileAvatar.Enabled && u.Meta.AvatarID == 0 && u.Meta.AvatarColor == "" { if err = svc.generateUserAvatarInitial(ctx, u); err != nil { return err } } if !svc.ac.CanReadUser(ctx, u) { return UserErrNotAllowedToRead() } if err = label.Load(ctx, svc.store, u); err != nil { return err } return nil }() return u, svc.recordAction(ctx, uaProps, UserActionLookup, err) } func (svc user) FindByEmail(ctx context.Context, email string) (u *types.User, err error) { var ( uaProps = &userActionProps{user: &types.User{Email: email}} ) err = func() error { u, err = store.LookupUserByEmail(ctx, svc.store, email) if u, err = svc.proc(ctx, u, err); err != nil { return err } uaProps.setUser(u) if !svc.ac.CanReadUser(ctx, u) { return UserErrNotAllowedToRead() } if err = label.Load(ctx, svc.store, u); err != nil { return err } return nil }() return u, svc.recordAction(ctx, uaProps, UserActionLookup, err) } func (svc user) FindByHandle(ctx context.Context, handle string) (u *types.User, err error) { var ( uaProps = &userActionProps{user: &types.User{Handle: handle}} ) err = func() error { u, err = store.LookupUserByHandle(ctx, svc.store, handle) if u, err = svc.proc(ctx, u, err); err != nil { return err } uaProps.setUser(u) if !svc.ac.CanReadUser(ctx, u) { return UserErrNotAllowedToRead() } if err = label.Load(ctx, svc.store, u); err != nil { return err } return nil }() return u, svc.recordAction(ctx, uaProps, UserActionLookup, err) } // FindByAny finds user by given identifier (context, id, handle, email) func (svc user) FindByAny(ctx context.Context, identifier interface{}) (u *types.User, err error) { if ctx, ok := identifier.(context.Context); ok { identifier = internalAuth.GetIdentityFromContext(ctx).Identity() } if ID, ok := identifier.(uint64); ok { u, err = svc.FindByID(ctx, ID) } else if identity, ok := identifier.(internalAuth.Identifiable); ok { u, err = svc.FindByID(ctx, identity.Identity()) } else if strIdentifier, ok := identifier.(string); ok { if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 { u, err = svc.FindByID(ctx, ID) } else if strings.Contains(strIdentifier, "@") { u, err = svc.FindByEmail(ctx, strIdentifier) } else { u, err = svc.FindByHandle(ctx, strIdentifier) } } else { err = UserErrInvalidID() } if err != nil { return } rr, _, err := store.SearchRoles(ctx, svc.store, types.RoleFilter{Resource: fmt.Sprintf("corteza::system:user/%d", u.ID)}) if err != nil { return nil, err } u.SetRoles(rr.IDs()...) return } func (svc user) proc(ctx context.Context, u *types.User, err error) (*types.User, error) { if err != nil { if errors.IsNotFound(err) { return nil, UserErrNotFound() } return nil, err } svc.handlePrivateData(ctx, u) return u, nil } // Find interacts with backend storage and // // @todo rename to Search() for consistency func (svc user) Find(ctx context.Context, filter types.UserFilter) (uu types.UserSet, f types.UserFilter, err error) { var ( uaProps = &userActionProps{filter: &filter} ) // For each fetched item, store backend will check if it is valid or not filter.MaskedEmailsEnabled = svc.settings.Privacy.Mask.Email filter.MaskedNamesEnabled = svc.settings.Privacy.Mask.Name filter.Check = func(res *types.User) (bool, error) { if !svc.ac.CanReadUser(ctx, res) { return false, nil } if svc.maskEmail(ctx, res) && ((len(filter.Query) > 0 && strings.HasPrefix(res.Email, filter.Query)) || res.Email == filter.Email) { // user email matched but it will be masked later on, so exclude it to prevent data probing return false, nil } if svc.maskName(ctx, res) && (len(filter.Query) > 0 && strings.HasPrefix(res.Name, filter.Query)) { // user mail matched but it will be masked later on, so exclude it to prevent data probing return false, nil } return true, nil } err = func() error { if !svc.ac.CanSearchUsers(ctx) { return UserErrNotAllowedToSearch() } if filter.Deleted > 0 { // If list with deleted users is requested // user must have access permissions to system (ie: is admin) // // not the best solution but ATM it allows us to have at least // some kind of control over who can see deleted users //if !svc.ac.CanAccess(ctx) { // return UserErrNotAllowedToListUsers() //} } if len(filter.Labels) > 0 { filter.LabeledIDs, err = label.Search( ctx, svc.store, types.User{}.LabelResourceKind(), filter.Labels, ) if err != nil { return err } // labels specified but no labeled resources found if len(filter.LabeledIDs) == 0 { return nil } } uu, f, err = store.SearchUsers(ctx, svc.store, filter) if err != nil { return err } if err = label.Load(ctx, svc.store, toLabeledUsers(uu)...); err != nil { return err } return uu.Walk(func(u *types.User) error { svc.handlePrivateData(ctx, u) return nil }) }() return uu, f, svc.recordAction(ctx, uaProps, UserActionSearch, err) } func (svc user) Create(ctx context.Context, new *types.User) (u *types.User, err error) { var ( uaProps = &userActionProps{user: new} ) err = func() (err error) { if !svc.ac.CanCreateUser(ctx) { return UserErrNotAllowedToCreate() } if new.Kind == types.SystemUser { return UserErrNotAllowedToCreateSystem() } if !handle.IsValid(new.Handle) { return UserErrInvalidHandle() } if _, err := mail.ParseAddress(new.Email); err != nil { return UserErrInvalidEmail() } if err = svc.checkLimits(ctx); err != nil { return err } if err = svc.eventbus.WaitFor(ctx, event.UserBeforeCreate(new, u)); err != nil { return } if new.Handle == "" { createUserHandle(ctx, DefaultStore, new) } if err = uniqueUserCheck(ctx, svc.store, new); err != nil { return } if new.Meta == nil { new.Meta = &types.UserMeta{} } // Process avatar initials Image if err = svc.generateUserAvatarInitial(ctx, new); err != nil { return } //add default user's theme new.Meta.Theme = sass.LightTheme new.ID = nextID() new.CreatedAt = *now() // consider email confirmed // when creating user like this new.EmailConfirmed = true if err = store.CreateUser(ctx, svc.store, new); err != nil { return } if err = label.Create(ctx, svc.store, new); err != nil { return } err = svc.userGroups.AssignGroupMembers(id.MustNumID(new.UserGroupID), id.MustNumID(new.ID)) if err != nil { return } _ = svc.eventbus.WaitFor(ctx, event.UserAfterCreate(new, u)) rbac.Global().InvalidateUser(new.ID) return }() return new, svc.recordAction(ctx, uaProps, UserActionCreate, err) } func (svc user) CreateWithAvatar(ctx context.Context, input *types.User, avatar io.Reader) (out *types.User, err error) { // @todo: avatar return svc.Create(ctx, input) } func (svc user) Update(ctx context.Context, upd *types.User) (u *types.User, err error) { var ( uaProps = &userActionProps{update: upd} ) err = func() (err error) { if !handle.IsValid(upd.Handle) { return UserErrInvalidHandle() } if _, err := mail.ParseAddress(upd.Email); err != nil { return UserErrInvalidEmail() } if u, err = loadUser(ctx, svc.store, upd.ID); err != nil { return } uaProps.setUser(u) if upd.Kind == types.SystemUser || u.Kind == types.SystemUser { return UserErrNotAllowedToUpdateSystem() } if upd.ID != internalAuth.GetIdentityFromContext(ctx).Identity() { if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() } } // Test if stale (update has an older version of data) if isStale(upd.UpdatedAt, u.UpdatedAt, u.CreatedAt) { return UserErrStaleData() } // Assign changed values u.Email = upd.Email u.Username = upd.Username u.Name = upd.Name u.Handle = upd.Handle u.UserGroupID = upd.UserGroupID u.Kind = upd.Kind u.UpdatedAt = now() if upd.Meta != nil { // Only update meta when set u.Meta = upd.Meta } if err = svc.generateUserAvatarInitial(ctx, u); err != nil { return } if err = svc.eventbus.WaitFor(ctx, event.UserBeforeUpdate(upd, u)); err != nil { return } if err = uniqueUserCheck(ctx, svc.store, u); err != nil { return } if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } if label.Changed(u.Labels, upd.Labels) { if err = label.Update(ctx, svc.store, upd); err != nil { return } u.Labels = upd.Labels } err = svc.userGroups.AssignGroupMembers(id.MustNumID(u.UserGroupID), id.MustNumID(u.ID)) if err != nil { return } _ = svc.eventbus.WaitFor(ctx, event.UserAfterUpdate(upd, u)) rbac.Global().InvalidateUser(u.ID) return }() return u, svc.recordAction(ctx, uaProps, UserActionUpdate, err) } func (svc user) ToggleEmailConfirmation(ctx context.Context, userID uint64, confirmed bool) (err error) { var ( u *types.User uaProps = &userActionProps{} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } uaProps.setUser(u) if userID != internalAuth.GetIdentityFromContext(ctx).Identity() { if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() } } u.EmailConfirmed = confirmed if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } return }() return svc.recordAction(ctx, uaProps, UserActionUpdate, err) } func (svc user) UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (out *types.User, err error) { // @todo: avatar return svc.Create(ctx, mod) } func (svc user) Delete(ctx context.Context, userID uint64) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } if u.Kind == types.SystemUser { return UserErrNotAllowedToDelete() } if !svc.ac.CanDeleteUser(ctx, u) { return UserErrNotAllowedToDelete() } if err = svc.eventbus.WaitFor(ctx, event.UserBeforeDelete(nil, u)); err != nil { return } u.DeletedAt = now() if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } err = svc.userGroups.RemoveGroupMembers(id.MustNumID(u.UserGroupID), id.MustNumID(u.ID)) if err != nil { return } if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil { return } _ = svc.eventbus.WaitFor(ctx, event.UserAfterDelete(nil, u)) rbac.Global().InvalidateUser(u.ID) return nil }() return svc.recordAction(ctx, uaProps, UserActionDelete, err) } func (svc user) Undelete(ctx context.Context, userID uint64) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } uaProps.setUser(u) if err = uniqueUserCheck(ctx, svc.store, u); err != nil { return err } if u.Kind == types.SystemUser { return UserErrNotAllowedToDelete() } if err = svc.checkLimits(ctx); err != nil { return err } if !svc.ac.CanDeleteUser(ctx, u) { return UserErrNotAllowedToDelete() } u.DeletedAt = nil if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } err = svc.userGroups.AssignGroupMembers(id.MustNumID(u.UserGroupID), id.MustNumID(u.ID)) if err != nil { return } rbac.Global().InvalidateUser(u.ID) return nil }() return svc.recordAction(ctx, uaProps, UserActionUndelete, err) } func (svc user) Suspend(ctx context.Context, userID uint64) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } uaProps.setUser(u) if u.Kind == types.SystemUser { return UserErrNotAllowedToSuspend() } if !svc.ac.CanSuspendUser(ctx, u) { return UserErrNotAllowedToSuspend() } // Clone u to oldUser oldUser := *u u.SuspendedAt = now() if err = svc.eventbus.WaitFor(ctx, event.UserBeforeSuspend(u, &oldUser)); err != nil { return } if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil { return } _ = svc.eventbus.WaitFor(ctx, event.UserAfterSuspend(u, &oldUser)) rbac.Global().InvalidateUser(u.ID) return nil }() return svc.recordAction(ctx, uaProps, UserActionSuspend, err) } func (svc user) Unsuspend(ctx context.Context, userID uint64) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } uaProps.setUser(u) if u.Kind == types.SystemUser { return UserErrNotAllowedToUnsuspend() } if !svc.ac.CanUnsuspendUser(ctx, u) { return UserErrNotAllowedToUnsuspend() } if err = svc.checkLimits(ctx); err != nil { return err } u.SuspendedAt = nil if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } rbac.Global().InvalidateUser(u.ID) return nil }() return svc.recordAction(ctx, uaProps, UserActionUnsuspend, err) } // SetPassword sets new password for a user // // Expecting setter to have permissions to update users func (svc user) SetPassword(ctx context.Context, userID uint64, newPassword string) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} a = UserActionSetPassword self = internalAuth.GetIdentityFromContext(ctx).Identity() == userID ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return err } uaProps.setUser(u) if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() } if u.Kind == types.SystemUser { return UserErrNotAllowedToUpdateSystem() } if !self { // when user is changing password for herself // we should not remove the tokens! // // without this, user needs to log-in again // and we do not want that if he is using general // user management API/UI if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil { return } } if newPassword == "" { a = UserActionRemovePassword return svc.auth.RemovePasswordCredentials(ctx, userID) } // note on password reuse: // // we do not really care if user is setting same password // to someone else (or to self for that matter) // // he has rights to update the user and is doing so // through general user management API if !svc.auth.CheckPasswordStrength(newPassword) { return UserErrPasswordNotSecure() } if err = svc.auth.SetPasswordCredentials(ctx, userID, newPassword); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, a, err) } // Masks (or leaves as-is) private data on user func (svc user) handlePrivateData(ctx context.Context, u *types.User) { if svc.maskEmail(ctx, u) { u.Email = maskPrivateDataEmail } if svc.maskName(ctx, u) { u.Name = maskPrivateDataName } } func (svc user) maskEmail(ctx context.Context, u *types.User) bool { return svc.settings.Privacy.Mask.Email && !svc.ac.CanUnmaskEmailOnUser(ctx, u) } func (svc user) maskName(ctx context.Context, u *types.User) bool { return svc.settings.Privacy.Mask.Name && !svc.ac.CanUnmaskNameOnUser(ctx, u) } func (svc *user) Get(ctx context.Context, h string) (u *types.User, err error) { if svc.preloaded[h] == nil { svc.preloaded[h], err = svc.FindByHandle(ctx, h) if err != nil { svc.preloaded[h] = &types.User{} return } } if svc.preloaded[h] == nil || svc.preloaded[h].ID == 0 { return nil, UserErrNotFound() } return svc.preloaded[h], nil } // DeleteAuthTokensByUserID will delete all auth tokens of user which will un-authorize all auth clients of user func (svc user) DeleteAuthTokensByUserID(ctx context.Context, userID uint64) (err error) { var ( uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if userID == 0 { return UserErrInvalidID() } if err = store.DeleteAuthOA2TokenByUserID(ctx, svc.store, userID); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, UserActionDeleteAuthTokens, err) } // DeleteAuthSessionsByUserID will delete all auth session of user func (svc user) DeleteAuthSessionsByUserID(ctx context.Context, userID uint64) (err error) { var ( uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if userID == 0 { return UserErrInvalidID() } if err = store.DeleteAuthSessionsByUserID(ctx, svc.store, userID); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, UserActionDeleteAuthSessions, err) } func (svc user) checkLimits(ctx context.Context) error { if svc.opt.LimitUsers == 0 { return nil } if c, err := countValidUsers(ctx, svc.store); err != nil { return err } else if c >= uint(svc.opt.LimitUsers) { return UserErrMaxUserLimitReached() } return nil } // CreateSynthetic generates, saves and returns new user // // Generated users will have their handles prefixed with "synthetic_" and email domain "synthetic.tld" // // Function checks if user can create users but avoids all other checks (besides unique value) func (svc user) CreateSynthetic(ctx context.Context, src synteticUserDataGen, total uint) error { if !svc.ac.CanCreateUser(ctx) { return UserErrNotAllowedToCreate() } const maxRetries = 10 return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { var retry uint for total > 0 || maxRetries < retry { // even with pre-check for unique users this one // still returns not unique error from time to time ?! err = store.CreateUser(ctx, s, syntheticUser(src)) if errors.IsDuplicateData(err) { retry++ continue } if err != nil { return } retry = 0 total-- } return }) } func syntheticUser(src synteticUserDataGen) (r *types.User) { r = &types.User{ ID: nextID(), Kind: types.NormalUser, Name: src.Name(), Handle: "synthetic_" + src.Username(), EmailConfirmed: src.Number(0, 1) > 0, // Make sure all users are created in the past CreatedAt: time.Now().Add(time.Hour * time.Duration(src.Number(100000, 1000000)*-1)), } r.Email = strings.ToLower(strings.ReplaceAll(r.Name, " ", ".")) + "@synthetic.tld" if src.Number(0, 1) > 0 { aux := time.Now().Add(time.Hour * time.Duration(src.Number(100, 100000)*-1)) r.UpdatedAt = &aux } return } // CreateSynthetic generates, saves and returns new user // // Generated users will have their handles prefixed with "synthetic_" and email domain "synthetic.tld" func (svc user) RemoveSynthetic(ctx context.Context) error { // not a mistake, we do not need or want to check if user can be deleted if !svc.ac.CanCreateUser(ctx) { return UserErrNotAllowedToCreate() } var ( f = types.UserFilter{Query: "@synthetic.tld"} uu types.UserSet ) f.Limit = 1000 // @todo this should be optimized by using store.DeleteUserByFilter return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { for { uu, _, err = store.SearchUsers(ctx, s, f) if len(uu) == 0 || err != nil { // when nothing is fetch or error returned // break out of the loop return } for _, u := range uu { // check if handle starts with synthetic_ if !strings.HasPrefix(u.Handle, "synthetic_") { continue } // check if email ends with synthetic.tld if !strings.HasSuffix(u.Email, "@synthetic.tld") { continue } if err = store.DeleteUser(ctx, s, u); err != nil { return } } } return }) } func loadUser(ctx context.Context, s store.Users, ID uint64) (res *types.User, err error) { if ID == 0 { return nil, UserErrInvalidID() } if res, err = store.LookupUserByID(ctx, s, ID); errors.IsNotFound(err) { return nil, UserErrNotFound() } return } func countValidUsers(ctx context.Context, s store.Users) (c uint, err error) { return store.CountUsers(ctx, s, types.UserFilter{Kind: types.NormalUser}) } // uniqueUserCheck verifies user's email, username and handle func uniqueUserCheck(ctx context.Context, s store.Storer, u *types.User) (err error) { isUnique := func(field string) bool { f := types.UserFilter{ // If user exists and is deleted -- not a dup Deleted: filter.StateExcluded, // If user exists and is suspended -- duplicate Suspended: filter.StateInclusive, } f.Limit = 1 switch field { case "email": if u.Email == "" { return true } f.Email = u.Email case "username": if u.Username == "" { return true } f.Username = u.Username case "handle": if u.Handle == "" { return true } f.Handle = u.Handle } set, _, err := store.SearchUsers(ctx, s, f) if err != nil || len(set) > 1 { // In case of error or multiple users returned return false } return len(set) == 0 || set[0].ID == u.ID } if !isUnique("email") { return UserErrEmailNotUnique() } if !isUnique("username") { return UserErrUsernameNotUnique() } if !isUnique("handle") { return UserErrHandleNotUnique() } return nil } func createUserHandle(ctx context.Context, s store.Users, u *types.User) { if u.Handle == "" { n := []string{ fmt.Sprintf("%s_%s", u.Name, u.Username), regexp. MustCompile("(@.*)$"). ReplaceAllString(u.Email, ""), } for i := 1; i <= 10; i++ { n = append(n, fmt.Sprintf("%s_%s%d", u.Name, u.Username, i)) } u.Handle, _ = handle.Cast( // Must not exist before func(lookup string) bool { e, err := s.LookupUserByHandle(ctx, lookup) return err == store.ErrNotFound && (e == nil || e.ID == u.ID) }, n..., ) } } // toLabeledUsers converts to []label.LabeledResource // // This function is auto-generated. func toLabeledUsers(set []*types.User) []label.LabeledResource { if len(set) == 0 { return nil } ll := make([]label.LabeledResource, len(set)) for i := range set { ll[i] = set[i] } return ll } func (svc user) UploadAvatar(ctx context.Context, userID uint64, upload *multipart.FileHeader) (err error) { var ( u *types.User att *types.Attachment uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } if userID != internalAuth.GetIdentityFromContext(ctx).Identity() { if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() } } if u.Meta.AvatarID != 0 { if err = svc.att.DeleteByID(ctx, u.Meta.AvatarID); err != nil { return } } file, err := upload.Open() if err != nil { return err } defer file.Close() att, err = svc.att.CreateAuthAttachment( ctx, upload.Filename, upload.Size, file, map[string]string{"key": types.AttachmentKindAvatar}, ) if err != nil { return err } u.Meta.AvatarID = att.ID u.Meta.AvatarKind = types.AttachmentKindAvatar if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, UserActionUploadAvatar, err) } // DeleteAvatar will delete user's avatar func (svc user) DeleteAvatar(ctx context.Context, userID uint64) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = svc.FindByID(ctx, userID); err != nil { return } if u.Kind == types.SystemUser { return UserErrNotAllowedToDeleteAvatar() } att, err := svc.att.FindByID(ctx, u.Meta.AvatarID) if err != nil { return err } if att.Meta.Labels["key"] != types.AttachmentKindAvatar { return nil } if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToDeleteAvatar() } if err = svc.att.DeleteByID(ctx, u.Meta.AvatarID); err != nil { return err } u.Meta.AvatarID = 0 // When an uploaded avatar is deleted, generate avatar initial if err = svc.generateUserAvatarInitial(ctx, u); err != nil { return err } if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, UserActionDeleteAvatar, err) } func processAvatarInitials(u *types.User) (initial string) { var ( chars string parts []string ) if u.Name != "" { parts = strings.Fields(u.Name) if len(parts) > 2 { chars = string(parts[0][0]) + string(parts[1][0]) + string(parts[2][0]) } else if len(parts) > 1 { chars = string(parts[0][0]) + string(parts[1][0]) } else { if len(parts[0]) > 1 { chars = string(parts[0][0]) + string(parts[0][1]) } else { chars = string(parts[0][0]) } } } else if u.Handle != "" { if strings.ContainsAny(u.Handle, "._-") { for _, del := range "._-" { if strings.Contains(u.Handle, string(del)) { parts = strings.Split(u.Handle, string(del)) break } } chars = string(parts[0][0]) + string(parts[1][0]) } else { chars = string(u.Handle[0]) } } else { email := strings.Split(u.Email, "@") if strings.ContainsAny(email[0], "._-") { for _, del := range "._-" { if strings.Contains(email[0], string(del)) { parts = strings.Split(email[0], string(del)) break } } chars = string(parts[0][0]) + string(parts[1][0]) } else { chars = string(email[0][0]) } } // Validate initials: if initials are letters if not assign a default "CU" for _, c := range chars { if unicode.IsLetter(c) { initial += string(c) } continue } if initial == "" { initial = "CU" } initial = strings.ToUpper(initial) return } func (svc user) GenerateAvatar(ctx context.Context, userID uint64, bgColor string, initialColor string) (err error) { var ( u *types.User uaProps = &userActionProps{user: &types.User{ID: userID}} ) err = func() (err error) { if u, err = loadUser(ctx, svc.store, userID); err != nil { return } if userID != internalAuth.GetIdentityFromContext(ctx).Identity() { if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() } } u.Meta.AvatarColor = initialColor u.Meta.AvatarBgColor = bgColor if err = svc.generateUserAvatarInitial(ctx, u); err != nil { return err } if err = store.UpdateUser(ctx, svc.store, u); err != nil { return } return nil }() return svc.recordAction(ctx, uaProps, UserActionGenerateAvatar, err) } func (svc user) generateUserAvatarInitial(ctx context.Context, u *types.User) (err error) { var ( att *types.Attachment ) initial := processAvatarInitials(u) if u.Meta == nil { u.Meta = &types.UserMeta{} } if u.Meta.AvatarID != 0 { if att, err = svc.att.FindByID(ctx, u.Meta.AvatarID); err != nil { return err } if att.Meta.Labels["key"] == types.AttachmentKindAvatar { return nil } colorLogic := att.Meta.Original.Image.BackgroundColor == u.Meta.AvatarBgColor && att.Meta.Original.Image.InitialColor == u.Meta.AvatarColor if att.Meta.Original.Image.Initial == initial && colorLogic { return nil } if err = svc.att.DeleteByID(ctx, att.ID); err != nil { return err } } if att, err = svc.att.CreateAvatarInitialsAttachment(ctx, initial, u.Meta.AvatarBgColor, u.Meta.AvatarColor); err != nil { return err } if u.Meta == nil { u.Meta = &types.UserMeta{} } u.Meta.AvatarID = att.ID u.Meta.AvatarKind = types.AttachmentKindAvatarInitials if u.Meta.AvatarBgColor == "" { u.Meta.AvatarBgColor = att.Meta.Original.Image.BackgroundColor } if u.Meta.AvatarColor == "" { u.Meta.AvatarColor = att.Meta.Original.Image.InitialColor } return nil }