1337 lines
31 KiB
Go
1337 lines
31 KiB
Go
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/label"
|
|
"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
|
|
|
|
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
|
|
}
|
|
|
|
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,
|
|
|
|
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{MemberID: 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
|
|
}
|
|
|
|
_ = svc.eventbus.WaitFor(ctx, event.UserAfterCreate(new, u))
|
|
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.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
|
|
}
|
|
|
|
_ = svc.eventbus.WaitFor(ctx, event.UserAfterUpdate(upd, u))
|
|
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
|
|
}
|
|
|
|
if err = svc.auth.RemoveAccessTokens(ctx, u); err != nil {
|
|
return
|
|
}
|
|
|
|
_ = svc.eventbus.WaitFor(ctx, event.UserAfterDelete(nil, u))
|
|
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
|
|
}
|
|
|
|
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))
|
|
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
|
|
}
|
|
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
|
|
}
|