// Package db provides database access for the IMC application. // It uses GORM (Go ORM) for database operations. // // The database contains two sets of tables: // 1. ISPmail tables (virtual_domains, virtual_users, virtual_aliases) - existing mail data // 2. imc tables (imc_users, imc_users2domains, etc.) - application-specific data package db import ( "fmt" // formatted error messages "time" // time package for timestamps "gorm.io/driver/mysql" // GORM MySQL driver "gorm.io/gorm" // GORM ORM library "gorm.io/gorm/logger" // GORM logger configuration "github.com/imc/backend/internal/config" // configuration ) // DB wraps GORM's database connection with helper methods. // This is the main database access point for the application. type DB struct { *gorm.DB // Embeds GORM's DB type, giving us all GORM methods } // ============================================================================= // ISPmail Models (existing mail server tables) // These tables are shared with the ISPmail system and must not be modified. // ============================================================================= // Domain 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 } // TableName tells GORM to use the existing ISPmail table name. func (Domain) TableName() string { return "virtual_domains" } // User 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 } // TableName tells GORM to use the existing ISPmail table name. func (User) TableName() string { return "virtual_users" } // Alias 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 } // TableName tells GORM to use the existing ISPmail table name. func (Alias) TableName() string { return "virtual_aliases" } // ============================================================================= // IMC Application Models (tables created by this app) // ============================================================================= // ImcUser represents an admin user who can log into this application. // This is separate from mail users - it's for the web admin interface. type ImcUser struct { ID uint `gorm:"primaryKey" json:"id"` // Primary key 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 CreatedAt time.Time `json:"createdAt"` // When account was created Domains []ImcUserDomain `gorm:"foreignKey:UserID" json:"domains,omitempty"` // Domains this user can access } // TableName specifies the database table name for ImcUser. 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) } // TableName specifies the database table name for ImcUserDomain. func (ImcUserDomain) TableName() string { return "imc_users2domains" } // ImcLoginAttempt records login attempts for brute-force protection. // 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 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 } // TableName specifies the database table name for ImcLoginAttempt. func (ImcLoginAttempt) TableName() string { return "imc_login_attempts" } // ============================================================================= // Helper/View Models (not stored in database) // ============================================================================= // DomainStats holds statistics about a domain (user count, alias count). // Used for displaying dashboard information. type DomainStats struct { ID uint `json:"id"` // Domain ID Name string `json:"name"` // Domain name UserCount int64 `json:"userCount"` // Number of mail users AliasCount int64 `json:"aliasCount"` // Number of aliases } // 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 } // 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 } // ============================================================================= // Database Connection // ============================================================================= // Connect establishes a connection to the MySQL/MariaDB database. // cfg: configuration containing database credentials and connection settings. // Returns: a DB wrapper around the GORM connection, or an error if connection fails. func Connect(cfg *config.Config) (*DB, error) { // Build the Data Source Name (DSN) string. // Format: username:password@protocol(address:port)/dbname?options // parseTime=true: automatically convert time.Time to/from SQL DATETIME // charset=utf8mb4: use full UTF-8 encoding (supports emojis, etc.) dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4", cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName) // Open a connection to the database using GORM. // We configure GORM to be silent (no auto-logging) to reduce noise. db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } // Get the underlying sql.DB object from GORM. // GORM wraps the standard database/sql package. sqlDB, err := db.DB() if err != nil { return nil, err } // Configure connection pool settings for performance. // These settings help balance between connection reuse and resource usage. // SetMaxOpenConns: maximum number of open connections to the database. // Too few = slow requests, too many = database overload. // 25 is a reasonable default for most applications. sqlDB.SetMaxOpenConns(25) // SetMaxIdleConns: maximum number of idle connections in the pool. // Idle connections are kept open even when not in use. // This avoids the overhead of opening new connections. sqlDB.SetMaxIdleConns(5) // SetConnMaxLifetime: maximum time a connection can be reused. // Connections older than this are closed and replaced. // This helps avoid issues with stale connections. sqlDB.SetConnMaxLifetime(5 * time.Minute) return &DB{db}, nil } // InitSchema creates IMC's database tables if they don't exist. // This only creates the imc_* tables, not the ISPmail virtual_* tables. // Called during application startup. func (d *DB) InitSchema() error { // Create imc_users table for admin accounts. // This table stores admin users who can log into the web interface. 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', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username), INDEX idx_role (role) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` if err := d.Exec(imcUsersSQL).Error; err != nil { return err } // Create imc_login_attempts table for tracking login attempts. // Used for brute-force protection (rate limiting). loginAttemptsSQL := ` CREATE TABLE IF NOT EXISTS imc_login_attempts ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email 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_ip_time (ip_address, attempted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` if err := d.Exec(loginAttemptsSQL).Error; err != nil { return err } // Create imc_users2domains table for user-domain permissions. // This implements many-to-many: a user can access multiple domains. users2DomainsSQL := ` CREATE TABLE IF NOT EXISTS imc_users2domains ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, domain_id INT UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_domain (user_id, domain_id), INDEX idx_user_id (user_id), INDEX idx_domain_id (domain_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` if err := d.Exec(users2DomainsSQL).Error; err != nil { return err } return nil }