diff --git a/Makefile b/Makefile index 006ba50..2d30a06 100644 --- a/Makefile +++ b/Makefile @@ -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..." diff --git a/backend/internal/api/handlers/domains.go b/backend/internal/api/handlers/domains.go index 391e652..7fbd35e 100644 --- a/backend/internal/api/handlers/domains.go +++ b/backend/internal/api/handlers/domains.go @@ -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, diff --git a/backend/internal/api/handlers/stats.go b/backend/internal/api/handlers/stats.go index 9f46497..b798c16 100644 --- a/backend/internal/api/handlers/stats.go +++ b/backend/internal/api/handlers/stats.go @@ -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() diff --git a/backend/internal/db/aliases.go b/backend/internal/db/aliases.go index 4a2be2a..21f5cea 100644 --- a/backend/internal/db/aliases.go +++ b/backend/internal/db/aliases.go @@ -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 } diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index c3021c5..83d4993 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -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 } // ============================================================================= diff --git a/backend/internal/db/domains.go b/backend/internal/db/domains.go index 5ccd230..8af42f9 100644 --- a/backend/internal/db/domains.go +++ b/backend/internal/db/domains.go @@ -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 } diff --git a/backend/internal/db/imc_users.go b/backend/internal/db/imc_users.go index 0fb7213..884b048 100644 --- a/backend/internal/db/imc_users.go +++ b/backend/internal/db/imc_users.go @@ -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 } diff --git a/backend/internal/db/users.go b/backend/internal/db/users.go index 7653f51..5b5f8be 100644 --- a/backend/internal/db/users.go +++ b/backend/internal/db/users.go @@ -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 }