WIP: Migrate from GORM to sqlc - partial migration, build broken
This commit is contained in:
parent
560b40503a
commit
dad96978e0
20 changed files with 1241 additions and 709 deletions
|
|
@ -1,198 +1,59 @@
|
|||
// 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
|
||||
// It uses sqlc for type-safe SQL queries.
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt" // formatted error messages
|
||||
"time" // time package for timestamps
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql" // GORM MySQL driver
|
||||
"gorm.io/gorm" // GORM ORM library
|
||||
"gorm.io/gorm/logger" // GORM logger configuration
|
||||
|
||||
"git.workaround.org/chaas/imc/backend/internal/config" // configuration
|
||||
"git.workaround.org/chaas/imc/backend/internal/config"
|
||||
imcdb "git.workaround.org/chaas/imc/backend/internal/db/sqlc"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// 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
|
||||
*imcdb.Queries
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ISPmail Models (existing mail server tables)
|
||||
// These tables are shared with the ISPmail system and must not be modified.
|
||||
// =============================================================================
|
||||
|
||||
// VirtualDomain represents a mail domain (e.g., "example.org").
|
||||
// This corresponds to the existing ISPmail virtual_domains table.
|
||||
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 (VirtualDomain) TableName() string { return "virtual_domains" }
|
||||
|
||||
// VirtualUser represents a mail user (e.g., "user@example.org").
|
||||
// This corresponds to the existing ISPmail virtual_users table.
|
||||
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 (VirtualUser) TableName() string { return "virtual_users" }
|
||||
|
||||
// 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 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 (VirtualAlias) 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
|
||||
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"` // Additional domains this user can access (many-to-many)
|
||||
}
|
||||
|
||||
// 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 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.
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UserCount int64 `json:"userCount"`
|
||||
AliasCount int64 `json:"aliasCount"`
|
||||
}
|
||||
|
||||
// AliasWithDomain combines an alias with its domain name for API responses.
|
||||
type AliasWithDomain struct {
|
||||
VirtualAlias // Embed VirtualAlias struct
|
||||
DomainName string `json:"domainName"` // Denormalized domain name for convenience
|
||||
imcdb.VirtualAlias
|
||||
DomainName string `json:"domainName"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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),
|
||||
})
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
return nil, fmt.Errorf("failed to open 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
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", 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
|
||||
return &DB{
|
||||
Queries: imcdb.New(db),
|
||||
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.
|
||||
// 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,
|
||||
|
|
@ -206,12 +67,10 @@ func (d *DB) InitSchema() error {
|
|||
INDEX idx_domain_id (domain_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
if err := d.Exec(imcUsersSQL).Error; err != nil {
|
||||
if _, err := d.db.Exec(imcUsersSQL); 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,
|
||||
|
|
@ -223,12 +82,10 @@ func (d *DB) InitSchema() error {
|
|||
INDEX idx_ip_time (ip_address, attempted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
if err := d.Exec(loginAttemptsSQL).Error; err != nil {
|
||||
if _, err := d.db.Exec(loginAttemptsSQL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
@ -240,9 +97,20 @@ func (d *DB) InitSchema() error {
|
|||
INDEX idx_domain_id (domain_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
if err := d.Exec(users2DomainsSQL).Error; err != nil {
|
||||
if _, err := d.db.Exec(users2DomainsSQL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue