Implements complete offline-first architecture with SQLite caching and MariaDB synchronization. Key features: - Local SQLite database for offline operation (data/quoteforge.db) - Connection settings with encrypted credentials - Component and pricelist caching with auto-sync - Sync API endpoints (/api/sync/status, /components, /pricelists, /all) - Real-time sync status indicator in UI with auto-refresh - Offline mode detection middleware - Migration tool for database initialization - Setup wizard for initial configuration New components: - internal/localdb: SQLite repository layer (components, pricelists, sync) - internal/services/sync: Synchronization service - internal/handlers/sync: Sync API handlers - internal/handlers/setup: Setup wizard handlers - internal/middleware/offline: Offline detection - cmd/migrate: Database migration tool UI improvements: - Setup page for database configuration - Sync status indicator with online/offline detection - Warning icons for pending synchronization - Auto-refresh every 30 seconds Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
package models
|
|
|
|
import (
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AllModels returns all models for auto-migration
|
|
func AllModels() []interface{} {
|
|
return []interface{}{
|
|
&User{},
|
|
&Category{},
|
|
&LotMetadata{},
|
|
&Configuration{},
|
|
&PriceOverride{},
|
|
&PricingAlert{},
|
|
&ComponentUsageStats{},
|
|
&Pricelist{},
|
|
&PricelistItem{},
|
|
}
|
|
}
|
|
|
|
// Migrate runs auto-migration for all QuoteForge tables
|
|
// Handles MySQL constraint errors gracefully for existing tables
|
|
func Migrate(db *gorm.DB) error {
|
|
for _, model := range AllModels() {
|
|
if err := db.AutoMigrate(model); err != nil {
|
|
// Skip known MySQL constraint errors for existing tables
|
|
errStr := err.Error()
|
|
if strings.Contains(errStr, "Can't DROP") ||
|
|
strings.Contains(errStr, "Duplicate key name") ||
|
|
strings.Contains(errStr, "check that it exists") {
|
|
slog.Warn("migration warning (skipped)", "model", model, "error", errStr)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SeedCategories inserts default categories if not exist
|
|
func SeedCategories(db *gorm.DB) error {
|
|
for _, cat := range DefaultCategories {
|
|
result := db.Where("code = ?", cat.Code).FirstOrCreate(&cat)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SeedAdminUser creates default admin user if not exists
|
|
// Default credentials: admin / admin123
|
|
func SeedAdminUser(db *gorm.DB, passwordHash string) error {
|
|
var count int64
|
|
db.Model(&User{}).Where("username = ?", "admin").Count(&count)
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
admin := &User{
|
|
Username: "admin",
|
|
Email: "admin@example.com",
|
|
PasswordHash: passwordHash,
|
|
Role: RoleAdmin,
|
|
IsActive: true,
|
|
}
|
|
return db.Create(admin).Error
|
|
}
|
|
|
|
// EnsureDBUser creates or returns the user corresponding to the database connection username.
|
|
// This is used when RBAC is disabled - configurations are owned by the DB user.
|
|
// Returns the user ID that should be used for all operations.
|
|
func EnsureDBUser(db *gorm.DB, dbUsername string) (uint, error) {
|
|
if dbUsername == "" {
|
|
return 0, nil
|
|
}
|
|
|
|
var user User
|
|
err := db.Where("username = ?", dbUsername).First(&user).Error
|
|
if err == nil {
|
|
return user.ID, nil
|
|
}
|
|
|
|
// User doesn't exist, create it
|
|
user = User{
|
|
Username: dbUsername,
|
|
Email: dbUsername + "@db.local",
|
|
PasswordHash: "-", // No password - this is a DB user, not an app user
|
|
Role: RoleAdmin,
|
|
IsActive: true,
|
|
}
|
|
|
|
if err := db.Create(&user).Error; err != nil {
|
|
slog.Error("failed to create DB user", "username", dbUsername, "error", err)
|
|
return 0, err
|
|
}
|
|
|
|
slog.Info("created DB user for configurations", "username", dbUsername, "user_id", user.ID)
|
|
return user.ID, nil
|
|
}
|