package db // ============================================================================= // Admin User (ImcUser) Operations // These functions manage admin users who can log into the web interface. // ============================================================================= // GetImcUserByUsername looks up an admin user by their username. // Returns the user with their associated domain preloaded. // username: the username to search for. // Returns: the user and any error. func (d *DB) GetImcUserByUsername(username string) (*ImcUser, error) { var user ImcUser // Preload loads the related Domain to avoid N+1 queries. // Where + First is the generics API pattern. err := d.Where("username = ?", username).Preload("Domain").First(&user).Error if err != nil { return nil, err // Return nil user and the error } return &user, nil } // GetImcUserByID looks up an admin user by their ID. // Returns the user with their associated domain preloaded. // id: the user's primary key ID. // Returns: the user and any error. func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) { var user ImcUser // Where + First looks up by primary key using generics API. err := d.Where("id = ?", id).Preload("Domain").First(&user).Error if err != nil { return nil, err } return &user, nil } // CreateImcUser creates a new admin user account. // username: the login username (must be unique). // passwordHash: the bcrypt hash of the password (NOT the plain password). // role: "admin" or "user". // domainID: the ID of the domain this user can manage. // Returns: the created user and any error. func (d *DB) CreateImcUser(username, passwordHash, role string, domainID uint) (*ImcUser, error) { user := ImcUser{ Username: username, PasswordHash: passwordHash, Role: role, DomainID: domainID, } // Create inserts the record into the database. err := d.Create(&user).Error if err != nil { return nil, err } return &user, nil } // UpdateImcUserPassword updates the password hash for a user. // id: the user's ID. // passwordHash: the new bcrypt hash of the password. func (d *DB) UpdateImcUserPassword(id uint, passwordHash string) error { // Model specifies which table/struct to update. // Updates with map for generics API. return d.Model(&ImcUser{}).Updates(map[string]any{"password_hash": passwordHash}).Error } // DeleteImcUser removes an admin user from the database. // id: the user's ID to delete. func (d *DB) DeleteImcUser(id uint) error { return d.Delete(&ImcUser{}, id).Error // Delete by primary key } // UpsertAdminUser creates the admin user if it doesn't exist, or updates the password if it does. func (d *DB) UpsertAdminUser(username, passwordHash string, domainID uint) error { var user ImcUser err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error if err == nil { return d.Model(&ImcUser{}).Updates(map[string]any{"password_hash": passwordHash}).Error } _, err = d.CreateImcUser(username, passwordHash, "admin", domainID) return err } // ============================================================================= // Domain Access Control // These functions manage which domains users can access. // ============================================================================= // GetUserAccessibleDomains returns all domains a user can access. // Admin users can access all domains; regular users get their primary domain plus any from many-to-many. // userID: the ID of the user. // isAdmin: whether the user has admin privileges. // Returns: a list of accessible domains. func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]VirtualDomain, error) { // Admins get access to all domains. if isAdmin { var domains []VirtualDomain err := d.Order("name ASC").Find(&domains).Error if err != nil { return nil, err } return domains, nil } // Regular users get their primary domain from imc_users table. var user ImcUser err := d.Where("id = ?", userID).First(&user).Error if err != nil { return nil, err } var primaryDomain VirtualDomain err = d.Where("id = ?", user.DomainID).First(&primaryDomain).Error if err != nil { return nil, err } // Also get additional domains from many-to-many table. var additionalDomains []VirtualDomain err = d.Table("imc_users2domains"). Select("virtual_domains.*"). Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). Where("imc_users2domains.user_id = ?", userID). Find(&additionalDomains).Error if err != nil { return nil, err } // Merge primary domain with additional domains (avoiding duplicates). domainMap := make(map[uint]VirtualDomain) domainMap[primaryDomain.ID] = primaryDomain for _, d := range additionalDomains { domainMap[d.ID] = d } result := make([]VirtualDomain, 0, len(domainMap)) for _, d := range domainMap { result = append(result, d) } return result, nil } // CanAccessDomain checks if a user can access a specific domain. // userID: the ID of the user. // domainName: the name of the domain (e.g., "example.org"). // isAdmin: whether the user has admin privileges. // Returns: true if the user can access the domain, false otherwise. func (d *DB) CanAccessDomain(userID uint, domainName string, isAdmin bool) (bool, error) { // Admins can access all domains. if isAdmin { return true, nil } // Regular users check their primary domain first. var user ImcUser err := d.Where("id = ?", userID).First(&user).Error if err != nil { return false, err } var primaryDomain VirtualDomain err = d.Where("id = ? AND name = ?", user.DomainID, domainName).First(&primaryDomain).Error if err == nil { return true, nil // Primary domain matches } // Also check the many-to-many table. var count int64 err = d.Table("imc_users2domains"). Select("COUNT(*)"). Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // GetUsersForDomain returns all admin users who can access a specific domain. // domainID: the ID of the domain. // Returns: a list of users who can access this domain (via primary domain or many-to-many). func (d *DB) GetUsersForDomain(domainID uint) ([]ImcUser, error) { var users []ImcUser // First get users who have this as their primary domain. err := d.Where("domain_id = ?", domainID).Find(&users).Error if err != nil { return nil, err } // Also get users from the many-to-many table. var additionalUsers []ImcUser err = d.Table("imc_users2domains"). Select("imc_users.*"). Joins("JOIN imc_users ON imc_users.id = imc_users2domains.user_id"). Where("imc_users2domains.domain_id = ?", domainID). Find(&additionalUsers).Error if err != nil { return nil, err } // Merge the results (avoiding duplicates). userMap := make(map[uint]ImcUser) for _, u := range users { userMap[u.ID] = u } for _, u := range additionalUsers { if _, exists := userMap[u.ID]; !exists { userMap[u.ID] = u } } result := make([]ImcUser, 0, len(userMap)) for _, u := range userMap { result = append(result, u) } return result, nil } // AddUserToDomain grants a user access to a domain via many-to-many relationship. // userID: the ID of the user. // domainID: the ID of the domain. func (d *DB) AddUserToDomain(userID, domainID uint) error { ud := ImcUserDomain{ UserID: userID, DomainID: domainID, } return d.Create(&ud).Error } // RemoveUserFromDomain revokes a user's access to a domain via many-to-many relationship. // userID: the ID of the user. // domainID: the ID of the domain. func (d *DB) RemoveUserFromDomain(userID, domainID uint) error { return d.Where("user_id = ? AND domain_id = ?", userID, domainID).Delete(&ImcUserDomain{}).Error } // IsUserInDomain checks if a user already has access to a domain via many-to-many. // userID: the ID of the user. // domainID: the ID of the domain. // Returns: true if the user already has access, false otherwise. func (d *DB) IsUserInDomain(userID, domainID uint) (bool, error) { var count int64 err := d.Model(&ImcUserDomain{}).Where("user_id = ? AND domain_id = ?", userID, domainID).Count(&count).Error return count > 0, err }