Migrate GORM to generics API, simplify user-domain relationship, refactor models

This commit is contained in:
Christoph Haas 2026-03-28 17:21:00 +01:00
parent 19ce2224b8
commit 560b40503a
9 changed files with 192 additions and 117 deletions

View file

@ -89,7 +89,26 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Failed to hash password: %v", err) 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) log.Fatalf("Failed to reset admin password: %v", err)
} }
fmt.Printf("Admin password reset successfully.\n") fmt.Printf("Admin password reset successfully.\n")

View file

@ -176,7 +176,7 @@ func isLockedOut(ip, identifier string, database *db.DB) bool {
} }
database.Model(&db.ImcLoginAttempt{}). 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) Count(&count)
return count >= MaxLoginAttempts return count >= MaxLoginAttempts
@ -184,7 +184,7 @@ func isLockedOut(ip, identifier string, database *db.DB) bool {
func recordFailedAttempt(identifier, ip string, database *db.DB) { func recordFailedAttempt(identifier, ip string, database *db.DB) {
database.Create(&db.ImcLoginAttempt{ database.Create(&db.ImcLoginAttempt{
Email: identifier, Username: identifier,
IPAddress: ip, IPAddress: ip,
Successful: false, Successful: false,
}) })
@ -192,7 +192,7 @@ func recordFailedAttempt(identifier, ip string, database *db.DB) {
func clearFailedAttempts(identifier, ip string, database *db.DB) { func clearFailedAttempts(identifier, ip string, database *db.DB) {
database.Model(&db.ImcLoginAttempt{}). database.Model(&db.ImcLoginAttempt{}).
Where("email = ? OR ip_address = ?", identifier, ip). Where("username = ? OR ip_address = ?", identifier, ip).
Update("successful", true) Update("successful", true)
} }

View file

@ -22,13 +22,6 @@ type CreateDomainRequest struct {
Name string `json:"name" binding:"required"` 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) { func (h *DomainHandler) List(c *gin.Context) {
authCtx := GetAuthContext(c) authCtx := GetAuthContext(c)
if authCtx == nil { if authCtx == nil {
@ -188,6 +181,13 @@ func (h *DomainHandler) Delete(c *gin.Context) {
NoContent(c) 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) { func (h *DomainHandler) GetPermissions(c *gin.Context) {
authCtx := GetAuthContext(c) authCtx := GetAuthContext(c)
if authCtx == nil { if authCtx == nil {

View file

@ -270,22 +270,6 @@ func (h *UserHandler) Delete(c *gin.Context) {
NoContent(c) 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!#$%&'*+\\-=?^_~-]+$") var emailLocalPartRegex = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-=?^_~-]+$")
func validateEmailLocalPart(email string) error { func validateEmailLocalPart(email string) error {

View file

@ -80,8 +80,10 @@ type ImcUser struct {
Username string `gorm:"uniqueIndex;size:100;not null" json:"username"` // Login username 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) 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 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 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. // 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. // Used to track failed login attempts and implement rate limiting.
type ImcLoginAttempt struct { type ImcLoginAttempt struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key 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) 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 AttemptedAt time.Time `json:"attemptedAt"` // When the attempt occurred
Successful bool `gorm:"default:false" json:"successful"` // Whether the login succeeded 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 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 // Database Connection
// ============================================================================= // =============================================================================
@ -196,15 +192,18 @@ func Connect(cfg *config.Config) (*DB, error) {
func (d *DB) InitSchema() error { func (d *DB) InitSchema() error {
// Create imc_users table for admin accounts. // Create imc_users table for admin accounts.
// This table stores admin users who can log into the web interface. // This table stores admin users who can log into the web interface.
// Each user is associated with one domain they can manage.
imcUsersSQL := ` imcUsersSQL := `
CREATE TABLE IF NOT EXISTS imc_users ( CREATE TABLE IF NOT EXISTS imc_users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE, username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
role ENUM('admin','user') DEFAULT 'user', role ENUM('admin','user') DEFAULT 'user',
domain_id INT UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username), INDEX idx_username (username),
INDEX idx_role (role) INDEX idx_role (role),
INDEX idx_domain_id (domain_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
` `
if err := d.Exec(imcUsersSQL).Error; err != nil { if err := d.Exec(imcUsersSQL).Error; err != nil {
@ -216,11 +215,11 @@ func (d *DB) InitSchema() error {
loginAttemptsSQL := ` loginAttemptsSQL := `
CREATE TABLE IF NOT EXISTS imc_login_attempts ( CREATE TABLE IF NOT EXISTS imc_login_attempts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(100) NOT NULL, username VARCHAR(100) NOT NULL,
ip_address VARCHAR(45) NOT NULL, ip_address VARCHAR(45) NOT NULL,
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
successful BOOLEAN DEFAULT FALSE, 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) INDEX idx_ip_time (ip_address, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
` `
@ -228,8 +227,8 @@ func (d *DB) InitSchema() error {
return err return err
} }
// Create imc_users2domains table for user-domain permissions. // Create imc_users2domains table for additional user-domain permissions (many-to-many).
// This implements many-to-many: a user can access multiple domains. // A user can have access to multiple domains beyond their primary domain.
users2DomainsSQL := ` users2DomainsSQL := `
CREATE TABLE IF NOT EXISTS imc_users2domains ( CREATE TABLE IF NOT EXISTS imc_users2domains (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

View file

@ -6,27 +6,29 @@ package db
// ============================================================================= // =============================================================================
// GetImcUserByUsername looks up an admin user by their username. // 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. // username: the username to search for.
// Returns: the user and any error. // Returns: the user and any error.
func (d *DB) GetImcUserByUsername(username string) (*ImcUser, error) { func (d *DB) GetImcUserByUsername(username string) (*ImcUser, error) {
var user ImcUser var user ImcUser
// Preload loads related data (Domains) to avoid N+1 queries. // Preload loads the related Domain to avoid N+1 queries.
// Where creates a SQL WHERE clause, ? is a placeholder for the username. // Where + First is the generics API pattern.
if err := d.Preload("Domains.Domain").Where("username = ?", username).First(&user).Error; err != nil { err := d.Where("username = ?", username).Preload("Domain").First(&user).Error
if err != nil {
return nil, err // Return nil user and the error return nil, err // Return nil user and the error
} }
return &user, nil return &user, nil
} }
// GetImcUserByID looks up an admin user by their ID. // 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. // id: the user's primary key ID.
// Returns: the user and any error. // Returns: the user and any error.
func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) { func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) {
var user ImcUser var user ImcUser
// First(&user, id) looks up by primary key. // Where + First looks up by primary key using generics API.
if err := d.Preload("Domains.Domain").First(&user, id).Error; err != nil { err := d.Where("id = ?", id).Preload("Domain").First(&user).Error
if err != nil {
return nil, err return nil, err
} }
return &user, nil return &user, nil
@ -36,15 +38,18 @@ func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) {
// username: the login username (must be unique). // username: the login username (must be unique).
// passwordHash: the bcrypt hash of the password (NOT the plain password). // passwordHash: the bcrypt hash of the password (NOT the plain password).
// role: "admin" or "user". // role: "admin" or "user".
// domainID: the ID of the domain this user can manage.
// Returns: the created user and any error. // 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{ user := ImcUser{
Username: username, Username: username,
PasswordHash: passwordHash, PasswordHash: passwordHash,
Role: role, Role: role,
DomainID: domainID,
} }
// Create inserts the record into the database. // 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 nil, err
} }
return &user, nil 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. // passwordHash: the new bcrypt hash of the password.
func (d *DB) UpdateImcUserPassword(id uint, passwordHash string) error { func (d *DB) UpdateImcUserPassword(id uint, passwordHash string) error {
// Model specifies which table/struct to update. // Model specifies which table/struct to update.
// Where filters which rows to update. // Updates with map for generics API.
// Update only changes the specified fields. return d.Model(&ImcUser{}).Updates(map[string]any{"password_hash": passwordHash}).Error
return d.Model(&ImcUser{}).Where("id = ?", id).Update("password_hash", passwordHash).Error
} }
// DeleteImcUser removes an admin user from the database. // 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. // 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 var user ImcUser
err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error
if err == nil { 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 return err
} }
@ -83,7 +87,7 @@ func (d *DB) UpsertAdminUser(username, passwordHash string) error {
// ============================================================================= // =============================================================================
// GetUserAccessibleDomains returns all domains a user can access. // 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. // userID: the ID of the user.
// isAdmin: whether the user has admin privileges. // isAdmin: whether the user has admin privileges.
// Returns: a list of accessible domains. // 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. // Admins get access to all domains.
if isAdmin { if isAdmin {
var domains []VirtualDomain 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 nil, err
} }
return domains, nil return domains, nil
} }
// Regular users query the imc_users2domains table. // Regular users get their primary domain from imc_users table.
// We use raw SQL with Joins because GORM's association methods var user ImcUser
// don't handle our cross-database queries well. err := d.Where("id = ?", userID).First(&user).Error
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
if err != nil { if err != nil {
return nil, err 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. // 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 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 var count int64
err := d.Table("imc_users2domains"). err = d.Table("imc_users2domains").
Select("COUNT(*)"). // Count matching rows Select("COUNT(*)").
Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id"). // Join to domain table Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id").
Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName). // Match user and domain name Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName).
Count(&count).Error Count(&count).Error
if err != nil { if err != nil {
return false, err 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. // userID: the ID of the user.
// domainID: the ID of the domain. // domainID: the ID of the domain.
func (d *DB) AddUserToDomain(userID, domainID uint) error { 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 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. // userID: the ID of the user.
// domainID: the ID of the domain. // domainID: the ID of the domain.
func (d *DB) RemoveUserFromDomain(userID, domainID uint) error { 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 return d.Where("user_id = ? AND domain_id = ?", userID, domainID).Delete(&ImcUserDomain{}).Error
} }
// GetUsersForDomain returns all admin users who can access a specific domain. // IsUserInDomain checks if a user already has access to a domain via many-to-many.
// 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. // userID: the ID of the user.
// domainID: the ID of the domain. // domainID: the ID of the domain.
// Returns: true if the user already has access, false otherwise. // Returns: true if the user already has access, false otherwise.

View file

@ -12,7 +12,8 @@ package db
// Returns a slice of AliasWithDomain (includes domain name) or an error. // Returns a slice of AliasWithDomain (includes domain name) or an error.
func (d *DB) GetAllAliases() ([]AliasWithDomain, error) { func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
var aliases []VirtualAlias 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 return nil, err
} }
@ -38,7 +39,8 @@ func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
// Returns a slice of VirtualAlias or an error. // Returns a slice of VirtualAlias or an error.
func (d *DB) GetAliasesByDomain(domainID uint) ([]VirtualAlias, error) { func (d *DB) GetAliasesByDomain(domainID uint) ([]VirtualAlias, error) {
var aliases []VirtualAlias 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 return nil, err
} }
if aliases == nil { 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"). // Returns the VirtualAlias or an error (including "record not found").
func (d *DB) GetAliasByID(id uint) (*VirtualAlias, error) { func (d *DB) GetAliasByID(id uint) (*VirtualAlias, error) {
var alias VirtualAlias 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 nil, err
} }
return &alias, nil 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"). // Returns the VirtualAlias or an error (including "record not found").
func (d *DB) GetAliasBySource(source string) (*VirtualAlias, error) { func (d *DB) GetAliasBySource(source string) (*VirtualAlias, error) {
var alias VirtualAlias 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 nil, err
} }
return &alias, nil return &alias, nil
@ -78,7 +82,8 @@ func (d *DB) CreateAlias(source, destination string) (*VirtualAlias, error) {
Source: source, Source: source,
Destination: destination, Destination: destination,
} }
if err := d.Create(&alias).Error; err != nil { err := d.Create(&alias).Error
if err != nil {
return nil, err return nil, err
} }
return &alias, nil return &alias, nil
@ -95,7 +100,8 @@ func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Vi
Source: source, Source: source,
Destination: destination, Destination: destination,
} }
if err := d.Create(&alias).Error; err != nil { err := d.Create(&alias).Error
if err != nil {
return nil, err return nil, err
} }
return &alias, nil return &alias, nil

View file

@ -12,7 +12,8 @@ package db
// Returns a slice of DomainStats (includes counts) or an error. // Returns a slice of DomainStats (includes counts) or an error.
func (d *DB) GetAllDomains() ([]DomainStats, error) { func (d *DB) GetAllDomains() ([]DomainStats, error) {
var domains []VirtualDomain 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 nil, err
} }
@ -41,7 +42,8 @@ func (d *DB) GetAllDomains() ([]DomainStats, error) {
// Returns the VirtualDomain or an error (including "record not found"). // Returns the VirtualDomain or an error (including "record not found").
func (d *DB) GetDomainByID(id uint) (*VirtualDomain, error) { func (d *DB) GetDomainByID(id uint) (*VirtualDomain, error) {
var domain VirtualDomain 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 nil, err
} }
return &domain, nil 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"). // Returns the VirtualDomain or an error (including "record not found").
func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) { func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) {
var domain VirtualDomain 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 nil, err
} }
return &domain, nil return &domain, nil
@ -63,7 +66,8 @@ func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) {
// Returns the created VirtualDomain or an error. // Returns the created VirtualDomain or an error.
func (d *DB) CreateDomain(name string) (*VirtualDomain, error) { func (d *DB) CreateDomain(name string) (*VirtualDomain, error) {
domain := VirtualDomain{Name: name} domain := VirtualDomain{Name: name}
if err := d.Create(&domain).Error; err != nil { err := d.Create(&domain).Error
if err != nil {
return nil, err return nil, err
} }
return &domain, nil return &domain, nil
@ -77,13 +81,16 @@ func (d *DB) DeleteDomain(id uint) error {
tx := d.Begin() tx := d.Begin()
defer func() { tx.Rollback() }() 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 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 return err
} }
if err := tx.Delete(&VirtualDomain{}, id).Error; err != nil { err = tx.Delete(&VirtualDomain{}, id).Error
if err != nil {
return err return err
} }

View file

@ -26,7 +26,8 @@ func (d *DB) GetAllMailUsers() ([]VirtualUser, error) {
// Returns a slice of VirtualUser or an error. // Returns a slice of VirtualUser or an error.
func (d *DB) GetUsersByDomain(domainID uint) ([]VirtualUser, error) { func (d *DB) GetUsersByDomain(domainID uint) ([]VirtualUser, error) {
var users []VirtualUser 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 return nil, err
} }
if users == nil { 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"). // Returns the VirtualUser or an error (including "record not found").
func (d *DB) GetUserByID(id uint) (*VirtualUser, error) { func (d *DB) GetUserByID(id uint) (*VirtualUser, error) {
var user VirtualUser 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 nil, err
} }
return &user, nil 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"). // Returns the VirtualUser or an error (including "record not found").
func (d *DB) GetUserByEmail(email string) (*VirtualUser, error) { func (d *DB) GetUserByEmail(email string) (*VirtualUser, error) {
var user VirtualUser 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 nil, err
} }
return &user, nil 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. // passwordHash: the new bcrypt hash of the password.
// Returns an error if the update fails. // Returns an error if the update fails.
func (d *DB) UpdateUserPassword(id uint, passwordHash string) error { 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. // 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). // quota: new quota in bytes (0 = use system default).
// Returns an error if the update fails. // Returns an error if the update fails.
func (d *DB) UpdateUserQuota(id uint, quota int64) error { 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. // DeleteUser permanently removes a mail user from the database.