From 560b40503a07f2aaa38aeb29111ab49389810859 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sat, 28 Mar 2026 17:21:00 +0100 Subject: [PATCH] Migrate GORM to generics API, simplify user-domain relationship, refactor models --- backend/cmd/server/main.go | 21 ++- backend/internal/api/handlers/auth.go | 6 +- backend/internal/api/handlers/domains.go | 14 +- backend/internal/api/handlers/users.go | 16 --- backend/internal/db/db.go | 25 ++-- backend/internal/db/imc_users.go | 175 +++++++++++++++-------- backend/internal/db/virtual_aliases.go | 18 ++- backend/internal/db/virtual_domains.go | 21 ++- backend/internal/db/virtual_users.go | 13 +- 9 files changed, 192 insertions(+), 117 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 556da5d..43ae5f9 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -89,7 +89,26 @@ func main() { if err != nil { log.Fatalf("Failed to hash password: %v", err) } - if err := database.UpsertAdminUser("admin", hash); err != nil { + + // Get or create a default domain for the admin user. + domains, err := database.GetAllDomains() + if err != nil || len(domains) == 0 { + // Create a default domain if none exist. + domain, err := database.CreateDomain("localhost") + if err != nil { + log.Fatalf("Failed to create default domain: %v", err) + } + if err := database.UpsertAdminUser("admin", hash, domain.ID); err != nil { + log.Fatalf("Failed to reset admin password: %v", err) + } + fmt.Printf("Admin password reset successfully.\n") + fmt.Printf("Username: admin\n") + fmt.Printf("Password: %s\n", password) + fmt.Printf("Default domain created: localhost\n") + return + } + // Use the first domain found. + if err := database.UpsertAdminUser("admin", hash, domains[0].ID); err != nil { log.Fatalf("Failed to reset admin password: %v", err) } fmt.Printf("Admin password reset successfully.\n") diff --git a/backend/internal/api/handlers/auth.go b/backend/internal/api/handlers/auth.go index 4aa45b0..44c1cfc 100644 --- a/backend/internal/api/handlers/auth.go +++ b/backend/internal/api/handlers/auth.go @@ -176,7 +176,7 @@ func isLockedOut(ip, identifier string, database *db.DB) bool { } database.Model(&db.ImcLoginAttempt{}). - Where("email = ? AND attempted_at > ? AND successful = false", identifier, cutoff). + Where("username = ? AND attempted_at > ? AND successful = false", identifier, cutoff). Count(&count) return count >= MaxLoginAttempts @@ -184,7 +184,7 @@ func isLockedOut(ip, identifier string, database *db.DB) bool { func recordFailedAttempt(identifier, ip string, database *db.DB) { database.Create(&db.ImcLoginAttempt{ - Email: identifier, + Username: identifier, IPAddress: ip, Successful: false, }) @@ -192,7 +192,7 @@ func recordFailedAttempt(identifier, ip string, database *db.DB) { func clearFailedAttempts(identifier, ip string, database *db.DB) { database.Model(&db.ImcLoginAttempt{}). - Where("email = ? OR ip_address = ?", identifier, ip). + Where("username = ? OR ip_address = ?", identifier, ip). Update("successful", true) } diff --git a/backend/internal/api/handlers/domains.go b/backend/internal/api/handlers/domains.go index 7fbd35e..4bdec39 100644 --- a/backend/internal/api/handlers/domains.go +++ b/backend/internal/api/handlers/domains.go @@ -22,13 +22,6 @@ type CreateDomainRequest struct { Name string `json:"name" binding:"required"` } -type DomainPermissions struct { - DomainID uint `json:"domainId"` - DomainName string `json:"domainName"` - UserID uint `json:"userId"` - CanManage bool `json:"canManage"` -} - func (h *DomainHandler) List(c *gin.Context) { authCtx := GetAuthContext(c) if authCtx == nil { @@ -188,6 +181,13 @@ func (h *DomainHandler) Delete(c *gin.Context) { NoContent(c) } +type DomainPermissions struct { + DomainID uint `json:"domainId"` + DomainName string `json:"domainName"` + UserID uint `json:"userId"` + CanManage bool `json:"canManage"` +} + func (h *DomainHandler) GetPermissions(c *gin.Context) { authCtx := GetAuthContext(c) if authCtx == nil { diff --git a/backend/internal/api/handlers/users.go b/backend/internal/api/handlers/users.go index 57edbe5..ae0988b 100644 --- a/backend/internal/api/handlers/users.go +++ b/backend/internal/api/handlers/users.go @@ -270,22 +270,6 @@ func (h *UserHandler) Delete(c *gin.Context) { NoContent(c) } -func (h *UserHandler) ListAll(c *gin.Context) { - authCtx := GetAuthContext(c) - if authCtx == nil || !authCtx.IsAdmin() { - Error(c, http.StatusForbidden, "admin access required") - return - } - - users, err := h.db.GetAllMailUsers() - if err != nil { - Error(c, http.StatusInternalServerError, "database error") - return - } - - Success(c, users) -} - var emailLocalPartRegex = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-=?^_~-]+$") func validateEmailLocalPart(email string) error { diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index 83d4993..f1e24db 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -80,8 +80,10 @@ type ImcUser struct { Username string `gorm:"uniqueIndex;size:100;not null" json:"username"` // Login username PasswordHash string `gorm:"size:255;not null" json:"-"` // Bcrypt hash of password (- means exclude from JSON) Role string `gorm:"type:enum('admin','user');default:'user'" json:"role"` // Role: admin or regular user + DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to VirtualDomain (primary domain this user manages) + Domain *VirtualDomain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain CreatedAt time.Time `json:"createdAt"` // When account was created - Domains []ImcUserDomain `gorm:"foreignKey:UserID" json:"domains,omitempty"` // Domains this user can access + Domains []ImcUserDomain `gorm:"foreignKey:UserID" json:"domains,omitempty"` // Additional domains this user can access (many-to-many) } // TableName specifies the database table name for ImcUser. @@ -104,7 +106,7 @@ func (ImcUserDomain) TableName() string { return "imc_users2domains" } // Used to track failed login attempts and implement rate limiting. type ImcLoginAttempt struct { ID uint `gorm:"primaryKey" json:"id"` // Primary key - Email string `gorm:"size:100;not null" json:"email"` // Email/username that was used + Username string `gorm:"size:100;not null" json:"username"` // Username that was used IPAddress string `gorm:"size:45;not null" json:"ipAddress"` // IP address of the requester (IPv6 compatible) AttemptedAt time.Time `json:"attemptedAt"` // When the attempt occurred Successful bool `gorm:"default:false" json:"successful"` // Whether the login succeeded @@ -132,12 +134,6 @@ type AliasWithDomain struct { DomainName string `json:"domainName"` // Denormalized domain name for convenience } -// UserWithDomain combines a user with their domain name for API responses. -type UserWithDomain struct { - VirtualUser // Embed VirtualUser struct - DomainName string `json:"domainName"` // Denormalized domain name for convenience -} - // ============================================================================= // Database Connection // ============================================================================= @@ -196,15 +192,18 @@ func Connect(cfg *config.Config) (*DB, error) { func (d *DB) InitSchema() error { // Create imc_users table for admin accounts. // This table stores admin users who can log into the web interface. + // Each user is associated with one domain they can manage. imcUsersSQL := ` CREATE TABLE IF NOT EXISTS imc_users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role ENUM('admin','user') DEFAULT 'user', + domain_id INT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username), - INDEX idx_role (role) + INDEX idx_role (role), + INDEX idx_domain_id (domain_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` if err := d.Exec(imcUsersSQL).Error; err != nil { @@ -216,11 +215,11 @@ func (d *DB) InitSchema() error { loginAttemptsSQL := ` CREATE TABLE IF NOT EXISTS imc_login_attempts ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - email VARCHAR(100) NOT NULL, + username VARCHAR(100) NOT NULL, ip_address VARCHAR(45) NOT NULL, attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, successful BOOLEAN DEFAULT FALSE, - INDEX idx_email_time (email, attempted_at), + INDEX idx_username_time (username, attempted_at), INDEX idx_ip_time (ip_address, attempted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` @@ -228,8 +227,8 @@ func (d *DB) InitSchema() error { return err } - // Create imc_users2domains table for user-domain permissions. - // This implements many-to-many: a user can access multiple domains. + // Create imc_users2domains table for additional user-domain permissions (many-to-many). + // A user can have access to multiple domains beyond their primary domain. users2DomainsSQL := ` CREATE TABLE IF NOT EXISTS imc_users2domains ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, diff --git a/backend/internal/db/imc_users.go b/backend/internal/db/imc_users.go index 884b048..20c44b6 100644 --- a/backend/internal/db/imc_users.go +++ b/backend/internal/db/imc_users.go @@ -6,27 +6,29 @@ package db // ============================================================================= // GetImcUserByUsername looks up an admin user by their username. -// Returns the user with their associated domain permissions preloaded. +// 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 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 { + // 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 permissions preloaded. +// 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 - // First(&user, id) looks up by primary key. - if err := d.Preload("Domains.Domain").First(&user, id).Error; err != nil { + // 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 @@ -36,15 +38,18 @@ func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) { // 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) (*ImcUser, 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. - if err := d.Create(&user).Error; err != nil { + err := d.Create(&user).Error + if err != nil { return nil, err } return &user, nil @@ -55,9 +60,8 @@ func (d *DB) CreateImcUser(username, passwordHash, role string) (*ImcUser, error // 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 + // 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. @@ -67,13 +71,13 @@ func (d *DB) DeleteImcUser(id uint) error { } // 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 { +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{}).Where("id = ?", user.ID).Update("password_hash", passwordHash).Error + return d.Model(&ImcUser{}).Updates(map[string]any{"password_hash": passwordHash}).Error } - _, err = d.CreateImcUser(username, passwordHash, "admin") + _, err = d.CreateImcUser(username, passwordHash, "admin", domainID) return err } @@ -83,7 +87,7 @@ func (d *DB) UpsertAdminUser(username, passwordHash string) error { // ============================================================================= // GetUserAccessibleDomains returns all domains a user can access. -// Admin users can access all domains; regular users only their assigned domains. +// 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. @@ -91,29 +95,49 @@ func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]VirtualDomai // Admins get access to all domains. if isAdmin { var domains []VirtualDomain - if err := d.Order("name ASC").Find(&domains).Error; err != nil { + err := d.Order("name ASC").Find(&domains).Error + if 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 []VirtualDomain - 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 + // 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 } - if domains == nil { - domains = []VirtualDomain{} // Return empty slice, not nil + + var primaryDomain VirtualDomain + err = d.Where("id = ?", user.DomainID).First(&primaryDomain).Error + if err != nil { + return nil, err } - return domains, nil + + // 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. @@ -127,20 +151,73 @@ func (d *DB) CanAccessDomain(userID uint, domainName string, isAdmin bool) (bool return true, nil } - // Count matching records - if count > 0, access is granted. + // 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(*)"). // 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 + 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 // True if at least one match found + return count > 0, nil } -// AddUserToDomain grants a user access to a domain. +// 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 { @@ -151,34 +228,14 @@ func (d *DB) AddUserToDomain(userID, domainID uint) error { return d.Create(&ud).Error } -// RemoveUserFromDomain revokes a user's access to a domain. +// 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 { - // 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. +// 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. diff --git a/backend/internal/db/virtual_aliases.go b/backend/internal/db/virtual_aliases.go index a8ede67..465b6b8 100644 --- a/backend/internal/db/virtual_aliases.go +++ b/backend/internal/db/virtual_aliases.go @@ -12,7 +12,8 @@ package db // Returns a slice of AliasWithDomain (includes domain name) or an error. func (d *DB) GetAllAliases() ([]AliasWithDomain, error) { var aliases []VirtualAlias - if err := d.Preload("Domain").Find(&aliases).Error; err != nil { + err := d.Preload("Domain").Find(&aliases).Error + if err != nil { return nil, err } @@ -38,7 +39,8 @@ func (d *DB) GetAllAliases() ([]AliasWithDomain, error) { // Returns a slice of VirtualAlias or an error. func (d *DB) GetAliasesByDomain(domainID uint) ([]VirtualAlias, error) { var aliases []VirtualAlias - if err := d.Where("domain_id = ?", domainID).Find(&aliases).Error; err != nil { + err := d.Where("domain_id = ?", domainID).Find(&aliases).Error + if err != nil { return nil, err } if aliases == nil { @@ -52,7 +54,8 @@ func (d *DB) GetAliasesByDomain(domainID uint) ([]VirtualAlias, error) { // Returns the VirtualAlias or an error (including "record not found"). func (d *DB) GetAliasByID(id uint) (*VirtualAlias, error) { var alias VirtualAlias - if err := d.Preload("Domain").First(&alias, id).Error; err != nil { + err := d.Where("id = ?", id).Preload("Domain").First(&alias).Error + if err != nil { return nil, err } return &alias, nil @@ -63,7 +66,8 @@ func (d *DB) GetAliasByID(id uint) (*VirtualAlias, error) { // Returns the VirtualAlias or an error (including "record not found"). func (d *DB) GetAliasBySource(source string) (*VirtualAlias, error) { var alias VirtualAlias - if err := d.Where("source = ?", source).First(&alias).Error; err != nil { + err := d.Where("source = ?", source).First(&alias).Error + if err != nil { return nil, err } return &alias, nil @@ -78,7 +82,8 @@ func (d *DB) CreateAlias(source, destination string) (*VirtualAlias, error) { Source: source, Destination: destination, } - if err := d.Create(&alias).Error; err != nil { + err := d.Create(&alias).Error + if err != nil { return nil, err } return &alias, nil @@ -95,7 +100,8 @@ func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Vi Source: source, Destination: destination, } - if err := d.Create(&alias).Error; err != nil { + err := d.Create(&alias).Error + if err != nil { return nil, err } return &alias, nil diff --git a/backend/internal/db/virtual_domains.go b/backend/internal/db/virtual_domains.go index 9108be2..90024c5 100644 --- a/backend/internal/db/virtual_domains.go +++ b/backend/internal/db/virtual_domains.go @@ -12,7 +12,8 @@ package db // Returns a slice of DomainStats (includes counts) or an error. func (d *DB) GetAllDomains() ([]DomainStats, error) { var domains []VirtualDomain - if err := d.Order("name ASC").Find(&domains).Error; err != nil { + err := d.Order("name ASC").Find(&domains).Error + if err != nil { return nil, err } @@ -41,7 +42,8 @@ func (d *DB) GetAllDomains() ([]DomainStats, error) { // Returns the VirtualDomain or an error (including "record not found"). func (d *DB) GetDomainByID(id uint) (*VirtualDomain, error) { var domain VirtualDomain - if err := d.Where("id = ?", id).First(&domain).Error; err != nil { + err := d.Where("id = ?", id).First(&domain).Error + if err != nil { return nil, err } return &domain, nil @@ -52,7 +54,8 @@ func (d *DB) GetDomainByID(id uint) (*VirtualDomain, error) { // Returns the VirtualDomain or an error (including "record not found"). func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) { var domain VirtualDomain - if err := d.Where("name = ?", name).First(&domain).Error; err != nil { + err := d.Where("name = ?", name).First(&domain).Error + if err != nil { return nil, err } return &domain, nil @@ -63,7 +66,8 @@ func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) { // Returns the created VirtualDomain or an error. func (d *DB) CreateDomain(name string) (*VirtualDomain, error) { domain := VirtualDomain{Name: name} - if err := d.Create(&domain).Error; err != nil { + err := d.Create(&domain).Error + if err != nil { return nil, err } return &domain, nil @@ -77,13 +81,16 @@ func (d *DB) DeleteDomain(id uint) error { tx := d.Begin() defer func() { tx.Rollback() }() - if err := tx.Where("domain_id = ?", id).Delete(&VirtualUser{}).Error; err != nil { + err := tx.Where("domain_id = ?", id).Delete(&VirtualUser{}).Error + if err != nil { return err } - if err := tx.Where("domain_id = ?", id).Delete(&VirtualAlias{}).Error; err != nil { + err = tx.Where("domain_id = ?", id).Delete(&VirtualAlias{}).Error + if err != nil { return err } - if err := tx.Delete(&VirtualDomain{}, id).Error; err != nil { + err = tx.Delete(&VirtualDomain{}, id).Error + if err != nil { return err } diff --git a/backend/internal/db/virtual_users.go b/backend/internal/db/virtual_users.go index a271e1b..ee7434f 100644 --- a/backend/internal/db/virtual_users.go +++ b/backend/internal/db/virtual_users.go @@ -26,7 +26,8 @@ func (d *DB) GetAllMailUsers() ([]VirtualUser, error) { // Returns a slice of VirtualUser or an error. func (d *DB) GetUsersByDomain(domainID uint) ([]VirtualUser, error) { var users []VirtualUser - if err := d.Where("domain_id = ?", domainID).Find(&users).Error; err != nil { + err := d.Where("domain_id = ?", domainID).Find(&users).Error + if err != nil { return nil, err } if users == nil { @@ -40,7 +41,8 @@ func (d *DB) GetUsersByDomain(domainID uint) ([]VirtualUser, error) { // Returns the VirtualUser or an error (including "record not found"). func (d *DB) GetUserByID(id uint) (*VirtualUser, error) { var user VirtualUser - if err := d.Preload("Domain").First(&user, id).Error; err != nil { + err := d.Where("id = ?", id).Preload("Domain").First(&user).Error + if err != nil { return nil, err } return &user, nil @@ -51,7 +53,8 @@ func (d *DB) GetUserByID(id uint) (*VirtualUser, error) { // Returns the VirtualUser or an error (including "record not found"). func (d *DB) GetUserByEmail(email string) (*VirtualUser, error) { var user VirtualUser - if err := d.Where("email = ?", email).First(&user).Error; err != nil { + err := d.Where("email = ?", email).First(&user).Error + if err != nil { return nil, err } return &user, nil @@ -98,7 +101,7 @@ func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainI // passwordHash: the new bcrypt hash of the password. // Returns an error if the update fails. func (d *DB) UpdateUserPassword(id uint, passwordHash string) error { - return d.Model(&VirtualUser{}).Where("id = ?", id).Update("password", passwordHash).Error + return d.Model(&VirtualUser{}).Updates(map[string]any{"password": passwordHash}).Error } // UpdateUserQuota updates the mailbox quota for a mail user. @@ -106,7 +109,7 @@ func (d *DB) UpdateUserPassword(id uint, passwordHash string) error { // quota: new quota in bytes (0 = use system default). // Returns an error if the update fails. func (d *DB) UpdateUserQuota(id uint, quota int64) error { - return d.Model(&VirtualUser{}).Where("id = ?", id).Update("quota", quota).Error + return d.Model(&VirtualUser{}).Updates(map[string]any{"quota": quota}).Error } // DeleteUser permanently removes a mail user from the database.