Rename ISPmail models to use Virtual* prefix

- Domain -> VirtualDomain
- User -> VirtualUser
- Alias -> VirtualAlias

This clarifies which tables are ISPmail tables vs IMC tables.
This commit is contained in:
Christoph Haas 2026-03-26 23:34:05 +01:00
parent 93922e4cad
commit d5880d5096
8 changed files with 87 additions and 96 deletions

View file

@ -32,18 +32,9 @@ build-backend: build-frontend
# Development targets
dev:
@echo "Starting backend dev server (hot-reload enabled)..."
cd $(BACKEND_DIR) && USE_EMBEDDED=false $(HOME)/go/bin/air > /tmp/imc-backend.log 2>&1 &
@echo "Backend started, PID: $$(pgrep -f air)"
sleep 3
cd $(BACKEND_DIR) && USE_EMBEDDED=false $(HOME)/go/bin/air
@echo "Starting frontend dev server..."
cd $(FRONTEND_DIR) && bun run dev > /tmp/imc-frontend.log 2>&1 &
@echo "Frontend started, PID: $$(pgrep -f 'vite')"
@echo ""
@echo "Logs:"
@echo " Backend: tail -f /tmp/imc-backend.log"
@echo " Frontend: tail -f /tmp/imc-frontend.log"
@echo ""
@echo "Stop with: pkill -f 'air|vite'"
cd $(FRONTEND_DIR) && bun run dev
dev-frontend:
@echo "Starting frontend dev server..."

View file

@ -46,8 +46,8 @@ func (h *DomainHandler) List(c *gin.Context) {
domainStats := make([]db.DomainStats, len(domains))
for i, d := range domains {
var userCount, aliasCount int64
h.db.Model(&db.User{}).Where("domain_id = ?", d.ID).Count(&userCount)
h.db.Model(&db.Alias{}).Where("domain_id = ?", d.ID).Count(&aliasCount)
h.db.Model(&db.VirtualUser{}).Where("domain_id = ?", d.ID).Count(&userCount)
h.db.Model(&db.VirtualAlias{}).Where("domain_id = ?", d.ID).Count(&aliasCount)
domainStats[i] = db.DomainStats{
ID: d.ID,
Name: d.Name,

View file

@ -28,7 +28,7 @@ func (h *StatsHandler) Get(c *gin.Context) {
users, err := h.db.GetAllMailUsers()
if err != nil {
users = []db.User{}
users = []db.VirtualUser{}
}
aliases, err := h.db.GetAllAliases()

View file

@ -1,7 +1,7 @@
package db
func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
var aliases []Alias
var aliases []VirtualAlias
if err := d.Preload("Domain").Find(&aliases).Error; err != nil {
return nil, err
}
@ -9,8 +9,8 @@ func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
result := make([]AliasWithDomain, len(aliases))
for i, a := range aliases {
result[i] = AliasWithDomain{
Alias: a,
DomainName: "",
VirtualAlias: a,
DomainName: "",
}
if a.Domain != nil {
result[i].DomainName = a.Domain.Name
@ -23,35 +23,35 @@ func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
return result, nil
}
func (d *DB) GetAliasesByDomain(domainID uint) ([]Alias, error) {
var aliases []Alias
func (d *DB) GetAliasesByDomain(domainID uint) ([]VirtualAlias, error) {
var aliases []VirtualAlias
if err := d.Where("domain_id = ?", domainID).Find(&aliases).Error; err != nil {
return nil, err
}
if aliases == nil {
aliases = []Alias{}
aliases = []VirtualAlias{}
}
return aliases, nil
}
func (d *DB) GetAliasByID(id uint) (*Alias, error) {
var alias Alias
func (d *DB) GetAliasByID(id uint) (*VirtualAlias, error) {
var alias VirtualAlias
if err := d.Preload("Domain").First(&alias, id).Error; err != nil {
return nil, err
}
return &alias, nil
}
func (d *DB) GetAliasBySource(source string) (*Alias, error) {
var alias Alias
func (d *DB) GetAliasBySource(source string) (*VirtualAlias, error) {
var alias VirtualAlias
if err := d.Where("source = ?", source).First(&alias).Error; err != nil {
return nil, err
}
return &alias, nil
}
func (d *DB) CreateAlias(source, destination string) (*Alias, error) {
alias := Alias{
func (d *DB) CreateAlias(source, destination string) (*VirtualAlias, error) {
alias := VirtualAlias{
Source: source,
Destination: destination,
}
@ -61,8 +61,8 @@ func (d *DB) CreateAlias(source, destination string) (*Alias, error) {
return &alias, nil
}
func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Alias, error) {
alias := Alias{
func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*VirtualAlias, error) {
alias := VirtualAlias{
DomainID: domainID,
Source: source,
Destination: destination,
@ -74,5 +74,5 @@ func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Al
}
func (d *DB) DeleteAlias(id uint) error {
return d.Delete(&Alias{}, id).Error
return d.Delete(&VirtualAlias{}, id).Error
}

View file

@ -28,46 +28,46 @@ type DB struct {
// These tables are shared with the ISPmail system and must not be modified.
// =============================================================================
// Domain represents a mail domain (e.g., "example.org").
// VirtualDomain represents a mail domain (e.g., "example.org").
// This corresponds to the existing ISPmail virtual_domains table.
type Domain struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key, auto-increment
Name string `gorm:"uniqueIndex;size:50;not null" json:"name"` // Domain name, must be unique
CreatedAt time.Time `json:"createdAt"` // When the domain was added
Users []User `gorm:"foreignKey:DomainID" json:"users,omitempty"` // Mail users in this domain
Aliases []Alias `gorm:"foreignKey:DomainID" json:"aliases,omitempty"` // Aliases in this domain
type VirtualDomain struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key, auto-increment
Name string `gorm:"uniqueIndex;size:50;not null" json:"name"` // Domain name, must be unique
CreatedAt time.Time `json:"createdAt"` // When the domain was added
Users []VirtualUser `gorm:"foreignKey:DomainID" json:"users,omitempty"` // Mail users in this domain
Aliases []VirtualAlias `gorm:"foreignKey:DomainID" json:"aliases,omitempty"` // Aliases in this domain
}
// TableName tells GORM to use the existing ISPmail table name.
func (Domain) TableName() string { return "virtual_domains" }
func (VirtualDomain) TableName() string { return "virtual_domains" }
// User represents a mail user (e.g., "user@example.org").
// VirtualUser represents a mail user (e.g., "user@example.org").
// This corresponds to the existing ISPmail virtual_users table.
type User struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"` // Full email address, must be unique
Password string `gorm:"size:150;not null" json:"-"` // Mailbox password (bcrypt hash, - means exclude from JSON)
Quota int64 `gorm:"default:0" json:"quota"` // Mailbox size limit in bytes (0 = default)
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
type VirtualUser struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to VirtualDomain
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"` // Full email address, must be unique
Password string `gorm:"size:150;not null" json:"-"` // Mailbox password (bcrypt hash, - means exclude from JSON)
Quota int64 `gorm:"default:0" json:"quota"` // Mailbox size limit in bytes (0 = default)
Domain *VirtualDomain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
}
// TableName tells GORM to use the existing ISPmail table name.
func (User) TableName() string { return "virtual_users" }
func (VirtualUser) TableName() string { return "virtual_users" }
// Alias represents an email alias/forwarding rule.
// VirtualAlias represents an email alias/forwarding rule.
// Maps one email address (source) to another (destination).
// This corresponds to the existing ISPmail virtual_aliases table.
type Alias struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
Source string `gorm:"size:100;not null" json:"source"` // Original email address (alias)
Destination string `gorm:"size:100;not null" json:"destination"` // Forward-to email address
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
type VirtualAlias struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to VirtualDomain
Source string `gorm:"size:100;not null" json:"source"` // Original email address (alias)
Destination string `gorm:"size:100;not null" json:"destination"` // Forward-to email address
Domain *VirtualDomain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain
}
// TableName tells GORM to use the existing ISPmail table name.
func (Alias) TableName() string { return "virtual_aliases" }
func (VirtualAlias) TableName() string { return "virtual_aliases" }
// =============================================================================
// IMC Application Models (tables created by this app)
@ -90,11 +90,11 @@ func (ImcUser) TableName() string { return "imc_users" }
// ImcUserDomain represents the many-to-many relationship between admin users and domains.
// An admin user can have access to multiple domains, and a domain can be accessed by multiple users.
type ImcUserDomain struct {
ID uint `gorm:"primaryKey" json:"id"` // Primary key
UserID uint `gorm:"not null" json:"userId"` // Foreign key to ImcUser
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to Domain
CreatedAt time.Time `json:"createdAt"` // When access was granted
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain (for preloading)
ID uint `gorm:"primaryKey" json:"id"` // Primary key
UserID uint `gorm:"not null" json:"userId"` // Foreign key to ImcUser
DomainID uint `gorm:"not null" json:"domainId"` // Foreign key to VirtualDomain
CreatedAt time.Time `json:"createdAt"` // When access was granted
Domain *VirtualDomain `gorm:"foreignKey:DomainID" json:"domain,omitempty"` // Associated domain (for preloading)
}
// TableName specifies the database table name for ImcUserDomain.
@ -128,14 +128,14 @@ type DomainStats struct {
// AliasWithDomain combines an alias with its domain name for API responses.
type AliasWithDomain struct {
Alias // Embed Alias struct
DomainName string `json:"domainName"` // Denormalized domain name for convenience
VirtualAlias // Embed VirtualAlias struct
DomainName string `json:"domainName"` // Denormalized domain name for convenience
}
// UserWithDomain combines a user with their domain name for API responses.
type UserWithDomain struct {
User // Embed User struct
DomainName string `json:"domainName"` // Denormalized domain name for convenience
VirtualUser // Embed VirtualUser struct
DomainName string `json:"domainName"` // Denormalized domain name for convenience
}
// =============================================================================

View file

@ -1,7 +1,7 @@
package db
func (d *DB) GetAllDomains() ([]DomainStats, error) {
var domains []Domain
var domains []VirtualDomain
if err := d.Order("name ASC").Find(&domains).Error; err != nil {
return nil, err
}
@ -9,8 +9,8 @@ func (d *DB) GetAllDomains() ([]DomainStats, error) {
stats := make([]DomainStats, len(domains))
for i, dom := range domains {
var userCount, aliasCount int64
d.Model(&User{}).Where("domain_id = ?", dom.ID).Count(&userCount)
d.Model(&Alias{}).Where("domain_id = ?", dom.ID).Count(&aliasCount)
d.Model(&VirtualUser{}).Where("domain_id = ?", dom.ID).Count(&userCount)
d.Model(&VirtualAlias{}).Where("domain_id = ?", dom.ID).Count(&aliasCount)
stats[i] = DomainStats{
ID: dom.ID,
@ -26,24 +26,24 @@ func (d *DB) GetAllDomains() ([]DomainStats, error) {
return stats, nil
}
func (d *DB) GetDomainByID(id uint) (*Domain, error) {
var domain Domain
func (d *DB) GetDomainByID(id uint) (*VirtualDomain, error) {
var domain VirtualDomain
if err := d.Where("id = ?", id).First(&domain).Error; err != nil {
return nil, err
}
return &domain, nil
}
func (d *DB) GetDomainByName(name string) (*Domain, error) {
var domain Domain
func (d *DB) GetDomainByName(name string) (*VirtualDomain, error) {
var domain VirtualDomain
if err := d.Where("name = ?", name).First(&domain).Error; err != nil {
return nil, err
}
return &domain, nil
}
func (d *DB) CreateDomain(name string) (*Domain, error) {
domain := Domain{Name: name}
func (d *DB) CreateDomain(name string) (*VirtualDomain, error) {
domain := VirtualDomain{Name: name}
if err := d.Create(&domain).Error; err != nil {
return nil, err
}
@ -54,13 +54,13 @@ func (d *DB) DeleteDomain(id uint) error {
tx := d.Begin()
defer func() { tx.Rollback() }()
if err := tx.Where("domain_id = ?", id).Delete(&User{}).Error; err != nil {
if err := tx.Where("domain_id = ?", id).Delete(&VirtualUser{}).Error; err != nil {
return err
}
if err := tx.Where("domain_id = ?", id).Delete(&Alias{}).Error; err != nil {
if err := tx.Where("domain_id = ?", id).Delete(&VirtualAlias{}).Error; err != nil {
return err
}
if err := tx.Delete(&Domain{}, id).Error; err != nil {
if err := tx.Delete(&VirtualDomain{}, id).Error; err != nil {
return err
}

View file

@ -87,10 +87,10 @@ func (d *DB) UpsertAdminUser(username, passwordHash string) error {
// 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) {
func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]VirtualDomain, error) {
// Admins get access to all domains.
if isAdmin {
var domains []Domain
var domains []VirtualDomain
if err := d.Order("name ASC").Find(&domains).Error; err != nil {
return nil, err
}
@ -100,7 +100,7 @@ func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]Domain, erro
// 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
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
@ -111,7 +111,7 @@ func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]Domain, erro
return nil, err
}
if domains == nil {
domains = []Domain{} // Return empty slice, not nil
domains = []VirtualDomain{} // Return empty slice, not nil
}
return domains, nil
}

View file

@ -1,45 +1,45 @@
package db
func (d *DB) GetAllMailUsers() ([]User, error) {
var users []User
func (d *DB) GetAllMailUsers() ([]VirtualUser, error) {
var users []VirtualUser
if err := d.Preload("Domain").Find(&users).Error; err != nil {
return nil, err
}
if users == nil {
users = []User{}
users = []VirtualUser{}
}
return users, nil
}
func (d *DB) GetUsersByDomain(domainID uint) ([]User, error) {
var users []User
func (d *DB) GetUsersByDomain(domainID uint) ([]VirtualUser, error) {
var users []VirtualUser
if err := d.Where("domain_id = ?", domainID).Find(&users).Error; err != nil {
return nil, err
}
if users == nil {
users = []User{}
users = []VirtualUser{}
}
return users, nil
}
func (d *DB) GetUserByID(id uint) (*User, error) {
var user User
func (d *DB) GetUserByID(id uint) (*VirtualUser, error) {
var user VirtualUser
if err := d.Preload("Domain").First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
func (d *DB) GetUserByEmail(email string) (*User, error) {
var user User
func (d *DB) GetUserByEmail(email string) (*VirtualUser, error) {
var user VirtualUser
if err := d.Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (d *DB) CreateUser(email, passwordHash string, quota int64) (*User, error) {
user := User{
func (d *DB) CreateUser(email, passwordHash string, quota int64) (*VirtualUser, error) {
user := VirtualUser{
Email: email,
Password: passwordHash,
Quota: quota,
@ -50,8 +50,8 @@ func (d *DB) CreateUser(email, passwordHash string, quota int64) (*User, error)
return &user, nil
}
func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainID uint) (*User, error) {
user := User{
func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainID uint) (*VirtualUser, error) {
user := VirtualUser{
DomainID: domainID,
Email: email,
Password: passwordHash,
@ -64,13 +64,13 @@ func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainI
}
func (d *DB) UpdateUserPassword(id uint, passwordHash string) error {
return d.Model(&User{}).Where("id = ?", id).Update("password", passwordHash).Error
return d.Model(&VirtualUser{}).Where("id = ?", id).Update("password", passwordHash).Error
}
func (d *DB) UpdateUserQuota(id uint, quota int64) error {
return d.Model(&User{}).Where("id = ?", id).Update("quota", quota).Error
return d.Model(&VirtualUser{}).Where("id = ?", id).Update("quota", quota).Error
}
func (d *DB) DeleteUser(id uint) error {
return d.Delete(&User{}, id).Error
return d.Delete(&VirtualUser{}, id).Error
}