package user import ( "context" "strings" "time" "github.com/abhinavxd/libredesk/internal/dbutil" "github.com/abhinavxd/libredesk/internal/envelope" "github.com/abhinavxd/libredesk/internal/user/models" "github.com/lib/pq" "github.com/volatiletech/null/v9" "golang.org/x/crypto/bcrypt" ) // MonitorUserAvailability continuously checks for user activity and sets them offline if inactive for more than 5 minutes. func (u *Manager) MonitorUserAvailability(ctx context.Context, onUsersOffline func([]int)) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: if userIDs := u.MarkInactiveUsersOffline(); len(userIDs) > 0 && onUsersOffline != nil { onUsersOffline(userIDs) } case <-ctx.Done(): return } } } // GetAgent retrieves an agent by ID and also caches it for future requests. func (u *Manager) GetAgent(id int, email string) (models.User, error) { agent, err := u.Get(id, email, []string{models.UserTypeAgent}) if err != nil { return models.User{}, err } u.agentCacheMu.Lock() u.agentCache[id] = agent u.agentCacheMu.Unlock() return agent, nil } // GetAgentFromCache retrieves an agent from the cache by ID. func (u *Manager) GetAgentFromCache(id int) (models.User, bool) { u.agentCacheMu.RLock() defer u.agentCacheMu.RUnlock() agent, exists := u.agentCache[id] if !exists { return models.User{}, false } return agent, true } // GetAgentCachedOrLoad retrieves an agent from cache, falling back to DB if not cached. func (u *Manager) GetAgentCachedOrLoad(id int) (models.User, error) { if agent, exists := u.GetAgentFromCache(id); exists { return agent, nil } return u.GetAgent(id, "") } // InvalidateAgentCache invalidates the agent cache for a specific agent ID. func (u *Manager) InvalidateAgentCache(id int) { u.agentCacheMu.Lock() defer u.agentCacheMu.Unlock() delete(u.agentCache, id) } // InvalidateAllAgentCache clears the entire agent cache. func (u *Manager) InvalidateAllAgentCache() { u.agentCacheMu.Lock() defer u.agentCacheMu.Unlock() u.agentCache = make(map[int]models.User) } // GetAgentsCompact returns a compact list of agents with limited fields. func (u *Manager) GetAgentsCompact() ([]models.UserCompact, error) { var users = make([]models.UserCompact, 0) if err := u.db.Select(&users, u.q.GetUsersCompact, pq.Array([]string{models.UserTypeAgent})); err != nil { u.lo.Error("error fetching users from db", "error", err) return users, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } return users, nil } // CreateAgent creates a new agent user. func (u *Manager) CreateAgent(firstName, lastName, email string, roles []string) (models.User, error) { password, err := u.generatePassword() if err != nil { u.lo.Error("error generating password", "error", err) return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } var id = 0 avatarURL := null.String{} email = strings.TrimSpace(strings.ToLower(email)) if err := u.q.InsertAgent.QueryRow(email, firstName, lastName, password, avatarURL, pq.Array(roles)).Scan(&id); err != nil { if dbutil.IsUniqueViolationError(err) { return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.T("user.sameEmailAlreadyExists"), nil) } u.lo.Error("error creating user", "error", err) return models.User{}, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } return u.Get(id, "", []string{models.UserTypeAgent}) } // UpdateAgent updates an agent with individual field parameters func (u *Manager) UpdateAgent(id int, firstName, lastName, email string, roles []string, enabled bool, availabilityStatus, newPassword string) error { var ( hashedPassword any err error ) // Set password? if newPassword != "" { if !IsStrongPassword(newPassword) { return envelope.NewError(envelope.InputError, PasswordHint, nil) } hashedPassword, err = bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { u.lo.Error("error generating bcrypt password", "error", err) return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } u.lo.Info("setting new password for user", "user_id", id) } // Update user in the database and clear cache. if _, err := u.q.UpdateAgent.Exec(id, firstName, lastName, email, pq.Array(roles), null.String{}, hashedPassword, enabled, availabilityStatus); err != nil { if dbutil.IsUniqueViolationError(err) { return envelope.NewError(envelope.GeneralError, u.i18n.T("user.sameEmailAlreadyExists"), nil) } u.lo.Error("error updating user", "error", err) return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } u.InvalidateAgentCache(id) return nil } // SoftDeleteAgent soft deletes an agent by ID. func (u *Manager) SoftDeleteAgent(id int) error { // Disallow if user is system user. systemUser, err := u.GetSystemUser() if err != nil { return err } if id == systemUser.ID { return envelope.NewError(envelope.InputError, u.i18n.T("user.cannotDeleteSystemUser"), nil) } if _, err := u.q.SoftDeleteAgent.Exec(id); err != nil { u.lo.Error("error deleting user", "error", err) return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil) } return nil } // MarkInactiveUsersOffline sets users offline if they have been inactive for more than 5 minutes. // Returns the IDs of users that were marked offline. func (u *Manager) MarkInactiveUsersOffline() []int { var userIDs []int if err := u.q.UpdateInactiveOffline.Select(&userIDs); err != nil { u.lo.Error("error setting users offline", "error", err) return nil } if len(userIDs) > 0 { u.lo.Info("set inactive users offline", "count", len(userIDs)) } return userIDs } // GetAllAgents returns a list of all agents. func (u *Manager) GetAgents() ([]models.UserCompact, error) { // Some dirty hack. return u.GetAllUsers(1, 999999999, []string{models.UserTypeAgent}, "desc", "users.updated_at", "") }