Files
libredesk/internal/user/user.go
T
Abhinav Raut 2e203607e5 fix duplicate contact creation on transient error during ext_id enrichment
CreateContact treated any SetExternalUserID failure as "ext_id taken" and
fell through to the upsert, so a transient DB error created a second
contact with the same email. Now only a unique violation or a contact
deleted mid-flight falls through, other errors are returned.
SetExternalUserID also reports whether a row was actually updated, and
dbutil error checks use errors.As so wrapped errors match. Widget JWTs
with no email now store NULL instead of an empty string.
2026-07-10 01:06:38 +05:30

647 lines
25 KiB
Go

// Package user managers all users in libredesk - agents and contacts.
package user
import (
"context"
"database/sql"
"embed"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"strings"
"sync"
"time"
"log"
"github.com/abhinavxd/libredesk/internal/dbutil"
"github.com/abhinavxd/libredesk/internal/envelope"
rmodels "github.com/abhinavxd/libredesk/internal/role/models"
"github.com/abhinavxd/libredesk/internal/stringutil"
"github.com/abhinavxd/libredesk/internal/user/models"
"github.com/jmoiron/sqlx"
"github.com/knadh/go-i18n"
"github.com/lib/pq"
"github.com/volatiletech/null/v9"
"github.com/zerodha/logf"
"golang.org/x/crypto/bcrypt"
)
var (
//go:embed queries.sql
efs embed.FS
minPassword = 10
maxPassword = 72
maxListPageSize = 500
// ErrPasswordTooLong is returned when the password passed to
// GenerateFromPassword is too long (i.e. > 72 bytes).
ErrPasswordTooLong = errors.New("password length exceeds 72 bytes")
PasswordHint = fmt.Sprintf("Password must be %d-%d characters long should contain at least one uppercase letter, one lowercase letter, one number, and one special character.", minPassword, maxPassword)
)
const (
lastActiveFlushDebounce = 30 * time.Second
agentCacheTTL = 10 * time.Minute
)
// Manager handles user-related operations.
type Manager struct {
lo *logf.Logger
i18n *i18n.I18n
q queries
db *sqlx.DB
agentCache map[int]cachedAgent
agentCacheMu sync.RWMutex
lastActiveFlushAt map[int]time.Time
lastActiveFlushAtMu sync.Mutex
}
type cachedAgent struct {
user models.User
expiresAt time.Time
}
// Opts contains options for initializing the Manager.
type Opts struct {
DB *sqlx.DB
Lo *logf.Logger
}
// queries contains prepared SQL queries.
type queries struct {
GetUser *sqlx.Stmt `query:"get-user"`
GetNotes *sqlx.Stmt `query:"get-notes"`
GetNote *sqlx.Stmt `query:"get-note"`
GetUserIDsByRole *sqlx.Stmt `query:"get-user-ids-by-role"`
GetUserByExternalID *sqlx.Stmt `query:"get-user-by-external-id"`
GetUsersCompact string `query:"get-users-compact"`
UpdateContact *sqlx.Stmt `query:"update-contact"`
UpdateContactBasicInfo *sqlx.Stmt `query:"update-contact-basic-info"`
UpdateAgent *sqlx.Stmt `query:"update-agent"`
UpdateCustomAttributes *sqlx.Stmt `query:"update-custom-attributes"`
UpsertCustomAttributes *sqlx.Stmt `query:"upsert-custom-attributes"`
UpdateAvatar *sqlx.Stmt `query:"update-avatar"`
UpdateAvailability *sqlx.Stmt `query:"update-availability"`
UpdateLastActiveAt *sqlx.Stmt `query:"update-last-active-at"`
UpdateInactiveOffline *sqlx.Stmt `query:"update-inactive-offline"`
GetAvailabilityStatus *sqlx.Stmt `query:"get-availability-status"`
UpdateLastLoginAt *sqlx.Stmt `query:"update-last-login-at"`
SoftDeleteAgent *sqlx.Stmt `query:"soft-delete-agent"`
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
SetResetPasswordToken *sqlx.Stmt `query:"set-reset-password-token"`
SetPassword *sqlx.Stmt `query:"set-password"`
DeleteNote *sqlx.Stmt `query:"delete-note"`
InsertAgent *sqlx.Stmt `query:"insert-agent"`
InsertContactWithExtID *sqlx.Stmt `query:"insert-contact-with-external-id"`
InsertContactNoExtID *sqlx.Stmt `query:"insert-contact-without-external-id"`
GetContactByEmail *sqlx.Stmt `query:"get-contact-by-email"`
GetContactByEmailWithoutExtID *sqlx.Stmt `query:"get-contact-by-email-without-ext-id"`
IsEmailBlocked *sqlx.Stmt `query:"is-email-blocked"`
SetExternalUserID *sqlx.Stmt `query:"set-external-user-id"`
InsertNote *sqlx.Stmt `query:"insert-note"`
InsertVisitor *sqlx.Stmt `query:"insert-visitor"`
GetVisitorByEmail *sqlx.Stmt `query:"get-visitor-by-email"`
UpgradeVisitorToContact *sqlx.Stmt `query:"upgrade-visitor-to-contact"`
ToggleEnable *sqlx.Stmt `query:"toggle-enable"`
// API key queries
GetUserByAPIKey *sqlx.Stmt `query:"get-user-by-api-key"`
SetAPIKey *sqlx.Stmt `query:"set-api-key"`
RevokeAPIKey *sqlx.Stmt `query:"revoke-api-key"`
UpdateAPIKeyLastUsed *sqlx.Stmt `query:"update-api-key-last-used"`
MergeVisitorToContact *sqlx.Stmt `query:"merge-visitor-to-contact"`
}
// New creates and returns a new instance of the Manager.
func New(i18n *i18n.I18n, opts Opts) (*Manager, error) {
var q queries
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
return nil, fmt.Errorf("error scanning SQL file: %w", err)
}
return &Manager{
q: q,
lo: opts.Lo,
i18n: i18n,
db: opts.DB,
agentCache: make(map[int]cachedAgent),
lastActiveFlushAt: make(map[int]time.Time),
}, nil
}
// VerifyPassword authenticates an user by email and password, returning the user if successful.
func (u *Manager) VerifyPassword(email string, password []byte) (models.User, error) {
var user models.User
if err := u.q.GetUser.Get(&user, 0, email, pq.Array([]string{models.UserTypeAgent})); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
}
u.lo.Error("error fetching user from db", "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
if err := u.verifyPassword(password, user.Password.String); err != nil {
return user, envelope.NewError(envelope.InputError, u.i18n.T("user.invalidEmailPassword"), nil)
}
return user, nil
}
// GetAllUsers returns a list of all users.
func (u *Manager) GetAllUsers(page, pageSize int, userTypes []string, order, orderBy string, filtersJSON, location string) ([]models.UserCompact, error) {
query, qArgs, err := u.makeUserListQuery(page, pageSize, userTypes, order, orderBy, filtersJSON, location)
if err != nil {
u.lo.Error("error creating user list query", "error", err)
return nil, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
// Start a read-only txn.
tx, err := u.db.BeginTxx(context.Background(), &sql.TxOptions{
ReadOnly: true,
})
if err != nil {
u.lo.Error("error starting read-only transaction", "error", err)
return nil, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
defer tx.Rollback()
// Execute query
var users = make([]models.UserCompact, 0)
if err := tx.Select(&users, query, qArgs...); err != nil {
u.lo.Error("error fetching users", "error", err)
return nil, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return users, nil
}
// Get retrieves an user by ID or email or type. At least one of ID or email must be provided.
func (u *Manager) Get(id int, email string, userType []string) (models.User, error) {
if id == 0 && email == "" {
return models.User{}, envelope.NewError(envelope.InputError, u.i18n.T("validation.invalidUser"), nil)
}
var user models.User
if err := u.q.GetUser.Get(&user, id, email, pq.Array(userType)); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return user, envelope.NewError(envelope.NotFoundError, u.i18n.T("validation.notFoundUser"), nil)
}
u.lo.Error("error fetching user from db", "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return user, nil
}
// GetContactOrVisitor retrieves a user by ID or email that is either a contact or visitor.
func (u *Manager) GetContactOrVisitor(id int, email string) (models.User, error) {
return u.Get(id, email, []string{models.UserTypeContact, models.UserTypeVisitor})
}
func (u *Manager) GetSystemUser() (models.User, error) {
return u.Get(0, models.SystemUserEmail, []string{models.UserTypeAgent})
}
// GetByExternalID retrieves a user by external user ID.
func (u *Manager) GetByExternalID(externalUserID string) (models.User, error) {
var user models.User
if err := u.q.GetUserByExternalID.Get(&user, externalUserID); err != nil {
if err == sql.ErrNoRows {
return user, envelope.NewError(envelope.NotFoundError, u.i18n.T("validation.notFoundUser"), nil)
}
u.lo.Error("error fetching user by external ID", "external_user_id", externalUserID, "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return user, nil
}
// GetContactByEmail retrieves a contact by email address regardless of external_user_id.
func (u *Manager) GetContactByEmail(email string) (models.User, error) {
var user models.User
if err := u.q.GetContactByEmail.Get(&user, email); err != nil {
if err == sql.ErrNoRows {
return user, envelope.NewError(envelope.NotFoundError, u.i18n.T("validation.notFoundUser"), nil)
}
u.lo.Error("error fetching contact by email", "email", email, "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return user, nil
}
// GetContactByEmailWithoutExtID retrieves a contact by email that has no external_user_id set.
func (u *Manager) GetContactByEmailWithoutExtID(email string) (models.User, error) {
var user models.User
if err := u.q.GetContactByEmailWithoutExtID.Get(&user, email); err != nil {
if err == sql.ErrNoRows {
return user, envelope.NewError(envelope.NotFoundError, u.i18n.T("validation.notFoundUser"), nil)
}
u.lo.Error("error fetching contact by email without ext_id", "email", email, "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return user, nil
}
// IsEmailBlocked checks if any contact or visitor with the given email is blocked.
func (u *Manager) IsEmailBlocked(email string) (bool, error) {
var blocked bool
if err := u.q.IsEmailBlocked.Get(&blocked, email); err != nil {
u.lo.Error("error checking if email is blocked", "email", email, "error", err)
return false, fmt.Errorf("checking if email is blocked: %w", err)
}
return blocked, nil
}
// GetVisitorByEmail retrieves a visitor by email address.
func (u *Manager) GetVisitorByEmail(email string) (models.User, error) {
var user models.User
if err := u.q.GetVisitorByEmail.Get(&user, email); err != nil {
if err == sql.ErrNoRows {
return user, envelope.NewError(envelope.NotFoundError, u.i18n.T("validation.notFoundUser"), nil)
}
u.lo.Error("error fetching visitor by email", "email", email, "error", err)
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return user, nil
}
// UpgradeVisitorToContact changes a visitor's type to contact.
func (u *Manager) UpgradeVisitorToContact(visitorID int) error {
if _, err := u.q.UpgradeVisitorToContact.Exec(visitorID); err != nil {
u.lo.Error("error upgrading visitor to contact", "visitor_id", visitorID, "error", err)
return fmt.Errorf("upgrading visitor to contact: %w", err)
}
return nil
}
// SetExternalUserID sets the external_user_id on an existing contact, reporting whether a row was updated.
func (u *Manager) SetExternalUserID(id int, externalUserID string) (bool, error) {
res, err := u.q.SetExternalUserID.Exec(id, externalUserID)
if err != nil {
u.lo.Error("error setting external user ID", "id", id, "external_user_id", externalUserID, "error", err)
return false, fmt.Errorf("setting external user ID: %w", err)
}
rows, err := res.RowsAffected()
if err != nil {
return false, fmt.Errorf("setting external user ID: %w", err)
}
return rows > 0, nil
}
// UpdateAvatar updates the user avatar.
func (u *Manager) UpdateAvatar(id int, path string) error {
if _, err := u.q.UpdateAvatar.Exec(id, null.NewString(path, path != "")); err != nil {
u.lo.Error("error updating user avatar", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// UpdateLastLoginAt updates the last login timestamp of an user.
func (u *Manager) UpdateLastLoginAt(id int) error {
if _, err := u.q.UpdateLastLoginAt.Exec(id); err != nil {
u.lo.Error("error updating user last login at", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// SetResetPasswordToken sets a reset password token for an user and returns the token.
func (u *Manager) SetResetPasswordToken(id int) (string, error) {
token, err := stringutil.RandomAlphanumeric(32)
if err != nil {
u.lo.Error("error generating reset password token", "error", err)
return "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
if _, err := u.q.SetResetPasswordToken.Exec(id, token); err != nil {
u.lo.Error("error setting reset password token", "error", err)
return "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return token, nil
}
// ResetPassword sets a password for a given user's reset password token and returns the user ID.
func (u *Manager) ResetPassword(token, password string) (int, error) {
if !IsStrongPassword(password) {
return 0, envelope.NewError(envelope.InputError, "Password is not strong enough, "+PasswordHint, nil)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
u.lo.Error("error generating bcrypt password", "error", err)
return 0, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
var id int
if err := u.q.SetPassword.Get(&id, passwordHash, token); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, envelope.NewError(envelope.InputError, u.i18n.T("user.resetPasswordTokenExpired"), nil)
}
u.lo.Error("error setting new password", "error", err)
return 0, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return id, nil
}
// UpdateAvailability updates the availability status of an user.
func (u *Manager) UpdateAvailability(id int, status string) error {
if _, err := u.q.UpdateAvailability.Exec(id, status); err != nil {
u.lo.Error("error updating user availability", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// UpdateLastActive updates last_active_at and returns true if the user flipped from offline to online.
func (u *Manager) UpdateLastActive(id int) (wasOffline bool, err error) {
agent, cachedOK := u.GetAgentFromCache(id)
alreadyOnline := cachedOK && agent.AvailabilityStatus == models.Online
// Already online and within the debounce window - nothing to do.
if alreadyOnline && !u.reserveFlush(id) {
return false, nil
}
if err := u.q.UpdateLastActiveAt.Get(&wasOffline, id); err != nil {
u.lo.Error("error updating user last active at", "error", err)
return false, fmt.Errorf("updating user last active at: %w", err)
}
if wasOffline {
u.InvalidateAgentCache(id)
}
return wasOffline, nil
}
// IsOffline returns true if the user's availability status is offline.
func (u *Manager) IsOffline(id int) bool {
var status string
if err := u.q.GetAvailabilityStatus.Get(&status, id); err != nil {
return true
}
return status == "offline"
}
// SaveCustomAttributes sets or merges custom attributes for a user.
// If replace is true, existing attributes are overwritten. Otherwise, attributes are merged.
func (u *Manager) SaveCustomAttributes(id int, customAttributes map[string]any, replace bool) error {
jsonb, err := json.Marshal(customAttributes)
if err != nil {
u.lo.Error("error marshalling custom attributes", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
var execErr error
if replace {
_, execErr = u.q.UpdateCustomAttributes.Exec(id, jsonb)
} else {
_, execErr = u.q.UpsertCustomAttributes.Exec(id, jsonb)
}
if execErr != nil {
u.lo.Error("error saving custom attributes", "error", execErr)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// ToggleEnabled toggles the enabled status of an user.
func (u *Manager) ToggleEnabled(id int, typ string, enabled bool) error {
if _, err := u.q.ToggleEnable.Exec(id, typ, enabled); err != nil {
u.lo.Error("error toggling user enabled status", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// GenerateAPIKey generates a new API key and secret for a user
func (u *Manager) GenerateAPIKey(userID int) (string, string, error) {
// Generate API key (32 characters)
apiKey, err := stringutil.RandomAlphanumeric(32)
if err != nil {
u.lo.Error("error generating API key", "error", err, "user_id", userID)
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
// Generate API secret (64 characters)
apiSecret, err := stringutil.RandomAlphanumeric(64)
if err != nil {
u.lo.Error("error generating API secret", "error", err, "user_id", userID)
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
// Hash the API secret for storage
secretHash, err := bcrypt.GenerateFromPassword([]byte(apiSecret), bcrypt.DefaultCost)
if err != nil {
u.lo.Error("error hashing API secret", "error", err, "user_id", userID)
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
// Update user with API key.
if _, err := u.q.SetAPIKey.Exec(userID, apiKey, string(secretHash)); err != nil {
u.lo.Error("error saving API key", "error", err, "user_id", userID)
return "", "", envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return apiKey, apiSecret, nil
}
// ValidateAPIKey validates API key and secret and returns the user
func (u *Manager) ValidateAPIKey(apiKey, apiSecret string) (models.User, error) {
var user models.User
// Find user by API key.
if err := u.q.GetUserByAPIKey.Get(&user, apiKey); err != nil {
if err == sql.ErrNoRows {
return user, envelope.NewError(envelope.UnauthorizedError, u.i18n.T("validation.invalidCredential"), nil)
}
return user, envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
// Verify API secret.
if err := bcrypt.CompareHashAndPassword([]byte(user.APISecret.String), []byte(apiSecret)); err != nil {
return user, envelope.NewError(envelope.UnauthorizedError, u.i18n.T("validation.invalidCredential"), nil)
}
// Update last used timestamp.
if _, err := u.q.UpdateAPIKeyLastUsed.Exec(user.ID); err != nil {
u.lo.Error("failed to update API key last used timestamp", "error", err, "user_id", user.ID)
}
return user, nil
}
// RevokeAPIKey deactivates the API key for a user
func (u *Manager) RevokeAPIKey(userID int) error {
if _, err := u.q.RevokeAPIKey.Exec(userID); err != nil {
u.lo.Error("error revoking API key", "error", err, "user_id", userID)
return envelope.NewError(envelope.GeneralError, u.i18n.T("globals.messages.somethingWentWrong"), nil)
}
return nil
}
// MergeVisitorToContact transfers conversations from visitor to contact and deletes the visitor.
func (u *Manager) MergeVisitorToContact(visitorID, contactID int) error {
if _, err := u.q.MergeVisitorToContact.Exec(visitorID, contactID); err != nil {
u.lo.Error("error merging visitor to contact", "visitor_id", visitorID, "contact_id", contactID, "error", err)
return fmt.Errorf("merging visitor to contact: %w", err)
}
return nil
}
func (u *Manager) GetUserIDsByRole(roleID int) ([]int, error) {
var ids []int
if err := u.q.GetUserIDsByRole.Select(&ids, roleID); err != nil {
u.lo.Error("error fetching user ids by role", "role_id", roleID, "error", err)
return nil, err
}
return ids, nil
}
// ChangeSystemUserPassword updates the system user's password with a newly prompted one.
func ChangeSystemUserPassword(ctx context.Context, db *sqlx.DB) error {
// Prompt for password and get hashed password
hashedPassword, err := promptAndHashPassword(ctx)
if err != nil {
return err
}
// Update system user's password in the database.
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
return fmt.Errorf("error updating system user password: %v", err)
}
fmt.Println("password updated successfully. Login with email 'System' and the new password.")
return nil
}
// CreateSystemUser creates a system user with the provided password or a random one.
func CreateSystemUser(ctx context.Context, password string, db *sqlx.DB) error {
var err error
// Set random password if not provided.
if password == "" {
password, err = stringutil.RandomAlphanumeric(32)
if err != nil {
return fmt.Errorf("failed to generate system used password: %v", err)
}
} else {
log.Print("using provided password for system user")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash system user password: %v", err)
}
_, err = db.Exec(`
WITH sys_user AS (
INSERT INTO users (email, type, first_name, last_name, password)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
)
INSERT INTO user_roles (user_id, role_id)
SELECT sys_user.id, roles.id
FROM sys_user, roles
WHERE roles.name = $6`,
models.SystemUserEmail, models.UserTypeAgent, "System", "", hashedPassword, rmodels.RoleAdmin)
if err != nil {
return fmt.Errorf("failed to create system user: %v", err)
}
log.Print("system user created successfully. Use command 'libredesk --set-system-user-password' to set the password and login with email 'System'.")
return nil
}
// IsStrongPassword checks if the password meets the required strength for system user.
func IsStrongPassword(password string) bool {
if len(password) < minPassword || len(password) > maxPassword {
return false
}
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLowercase := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
// Matches special characters
hasSpecial := regexp.MustCompile(`[\W_]`).MatchString(password)
return hasUppercase && hasLowercase && hasNumber && hasSpecial
}
// promptAndHashPassword handles password input and validation, and returns the hashed password.
func promptAndHashPassword(ctx context.Context) ([]byte, error) {
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
fmt.Printf("Please set System user password (%s): ", PasswordHint)
buffer := make([]byte, 256)
n, err := os.Stdin.Read(buffer)
if err != nil {
return nil, fmt.Errorf("error reading input: %v", err)
}
password := strings.TrimSpace(string(buffer[:n]))
if IsStrongPassword(password) {
// Hash the password using bcrypt.
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %v", err)
}
return hashedPassword, nil
}
fmt.Println("Password does not meet the strength requirements.")
}
}
}
// updateSystemUserPassword updates the password of the system user in the database.
func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
_, err := db.Exec(`UPDATE users SET password = $1 WHERE email = $2`, hashedPassword, models.SystemUserEmail)
if err != nil {
return fmt.Errorf("failed to update system user password: %v", err)
}
return nil
}
// makeUserListQuery generates a query to fetch users based on the provided filters.
func (u *Manager) makeUserListQuery(page, pageSize int, userTypes []string, order, orderBy, filtersJSON, location string) (string, []interface{}, error) {
var qArgs []any
qArgs = append(qArgs, pq.Array(userTypes))
return dbutil.BuildPaginatedQuery(u.q.GetUsersCompact, qArgs, dbutil.PaginationOptions{
Order: order,
OrderBy: orderBy,
Page: page,
PageSize: pageSize,
Location: location,
}, filtersJSON, dbutil.AllowedFields{
"users": {"email", "created_at", "updated_at"},
}, nil)
}
// verifyPassword compares the provided password with the stored password hash.
func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
if err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd); err != nil {
u.lo.Error("error verifying password", "error", err)
return fmt.Errorf("error verifying password: %w", err)
}
return nil
}
// generatePassword generates a random password and returns its bcrypt hash.
func (u *Manager) generatePassword() ([]byte, error) {
password, _ := stringutil.RandomAlphanumeric(70)
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
u.lo.Error("error generating bcrypt password", "error", err)
return nil, fmt.Errorf("generating bcrypt password: %w", err)
}
return bytes, nil
}
// reserveFlush atomically claims the flush slot, returning false if still inside the debounce window.
func (u *Manager) reserveFlush(id int) bool {
u.lastActiveFlushAtMu.Lock()
defer u.lastActiveFlushAtMu.Unlock()
if last, ok := u.lastActiveFlushAt[id]; ok && time.Since(last) < lastActiveFlushDebounce {
return false
}
// Stamp timestamp.
u.lastActiveFlushAt[id] = time.Now()
return true
}