package db import "time" // ============================================================================= // 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 permissions 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 related data (Domains) to avoid N+1 queries. // Where creates a SQL WHERE clause, ? is a placeholder for the username. if err := d.Preload("Domains.Domain").Where("username = ?", username).First(&user).Error; 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 permissions 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 // First(&user, id) looks up by primary key. if err := d.Preload("Domains.Domain").First(&user, id).Error; 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". // Returns: the created user and any error. func (d *DB) CreateImcUser(username, passwordHash, role string) (*ImcUser, error) { user := ImcUser{ Username: username, PasswordHash: passwordHash, Role: role, } // Create inserts the record into the database. if err := d.Create(&user).Error; 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. // Where filters which rows to update. // Update only changes the specified fields. return d.Model(&ImcUser{}).Where("id = ?", id).Update("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) error { var user ImcUser err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error if err == nil { return d.Model(&ImcUser{}).Where("id = ?", user.ID).Update("password_hash", passwordHash).Error } _, err = d.CreateImcUser(username, passwordHash, "admin") 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 only their assigned domains. // 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) ([]Domain, error) { // Admins get access to all domains. if isAdmin { var domains []Domain if err := d.Order("name ASC").Find(&domains).Error; err != nil { return nil, err } return domains, nil } // Regular users query the imc_users2domains table. // We use raw SQL with Joins because GORM's association methods // don't handle our cross-database queries well. var domains []Domain err := d.Table("imc_users2domains"). // Query from this table Select("virtual_domains.*"). // Select all columns from domains Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). // Join to get domain details Where("imc_users2domains.user_id = ?", userID). // Only this user's domains Order("name ASC"). Find(&domains).Error if err != nil { return nil, err } if domains == nil { domains = []Domain{} // Return empty slice, not nil } return domains, 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 } // Count matching records - if count > 0, access is granted. var count int64 err := d.Table("imc_users2domains"). Select("COUNT(*)"). // Count matching rows Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). // Join to domain table Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName). // Match user and domain name Count(&count).Error if err != nil { return false, err } return count > 0, nil // True if at least one match found } // AddUserToDomain grants a user access to a domain. // 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. // userID: the ID of the user. // domainID: the ID of the domain. func (d *DB) RemoveUserFromDomain(userID, domainID uint) error { // Delete records matching both user_id and domain_id. return d.Where("user_id = ? AND domain_id = ?", userID, domainID).Delete(&ImcUserDomain{}).Error } // 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. func (d *DB) GetUsersForDomain(domainID uint) ([]ImcUser, error) { var users []ImcUser err := d.Table("imc_users2domains"). Select("imc_users.*"). // Select all columns from imc_users Joins("JOIN imc_users ON imc_users.id = imc_users2domains.user_id"). // Join to users table Where("imc_users2domains.domain_id = ?", domainID). Find(&users).Error if err != nil { return nil, err } if users == nil { users = []ImcUser{} } return users, nil } // IsUserInDomain checks if a user already has access to a domain. // 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 } // ============================================================================= // Password Reset Tokens // These functions manage one-time tokens for password reset. // ============================================================================= // CreatePasswordResetToken creates a new password reset token. // userID: the ID of the user requesting password reset. // token: the random token string. // expiresAt: when this token becomes invalid. func (d *DB) CreatePasswordResetToken(userID uint, token string, expiresAt time.Time) error { resetToken := PasswordResetToken{ UserID: userID, Token: token, ExpiresAt: expiresAt, } return d.Create(&resetToken).Error } // GetValidPasswordResetToken looks up a valid (unused, not expired) reset token. // token: the token string to look up. // Returns: the token if valid, or an error if not found/expired/used. func (d *DB) GetValidPasswordResetToken(token string) (*PasswordResetToken, error) { var resetToken PasswordResetToken // Check token exists, hasn't been used, and hasn't expired. err := d.Where("token = ? AND used = false AND expires_at > ?", token, time.Now()).First(&resetToken).Error if err != nil { return nil, err } return &resetToken, nil } // MarkResetTokenUsed marks a token as used (prevents reuse). // token: the token string to mark as used. func (d *DB) MarkResetTokenUsed(token string) error { return d.Model(&PasswordResetToken{}).Where("token = ?", token).Update("used", true).Error }