package database import ( "log" "time" "gauth-central/config" "gauth-central/internal/models" "golang.org/x/crypto/bcrypt" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) var DB *gorm.DB func ConnectDB() { dsn := config.AppConfig.DBUrl if dsn == "" { log.Fatal("DB_URL is not set in .env") } // Configure GORM with optimized settings db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Error), // Only log errors, suppress SLOW SQL warnings PrepareStmt: true, // Prepare statements for better performance NowFunc: func() time.Time { return time.Now().UTC() }, }) if err != nil { log.Fatal("Failed to connect to database:", err) } log.Println("Connected to Database successfully") DB = db // Enable UUID extension enableUUIDExtension() } func enableUUIDExtension() { // Enable uuid-ossp extension for uuid_generate_v4() err := DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error if err != nil { log.Printf("Warning: Could not enable uuid-ossp extension: %v", err) } else { log.Println("UUID extension enabled") } } func Migrate() { // Manual migration for user_name column to handle existing data migrateUserNameColumn() err := DB.AutoMigrate( &models.User{}, &models.SocialAccount{}, &models.Role{}, &models.Permission{}, &models.CorsWhitelist{}, &models.CorsBlacklist{}, &models.RateLimitSetting{}, ) if err != nil { log.Fatal("Database Migration Failed:", err) } log.Println("Database Migration Completed") // Migration for email_verified column is disabled after initial setup // New users will have email_verified=false by default for email/password registration // migrateEmailVerifiedColumn() seedRolesAndPermissions() seedDefaultSettings() // seedDefaultAdmin() - Removed from auto migration } func migrateEmailVerifiedColumn() { // Fast check using pg_catalog instead of information_schema var count int64 DB.Raw(` SELECT COUNT(*) FROM pg_attribute WHERE attrelid = 'users'::regclass AND attname = 'email_verified' AND NOT attisdropped `).Scan(&count) if count == 0 { return } // Only set existing users (created before email verification feature) as verified // Users with no verify token AND created before a certain date are old users // For simplicity: set all users without a verification token as verified (one-time migration) var usersToVerify int64 DB.Model(&models.User{}).Where("(email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL").Count(&usersToVerify) if usersToVerify > 0 { DB.Exec("UPDATE users SET email_verified = true WHERE (email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL") log.Printf("Email verification migration: %d existing users marked as verified", usersToVerify) } } func migrateUserNameColumn() { // Fast check using pg_catalog instead of information_schema var count int64 DB.Raw(` SELECT COUNT(*) FROM pg_attribute WHERE attrelid = 'users'::regclass AND attname = 'user_name' AND NOT attisdropped `).Scan(&count) if count == 0 { // Column doesn't exist, add it log.Println("Adding user_name column...") DB.Exec("ALTER TABLE users ADD COLUMN user_name text") // Update existing users with default usernames DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(id::text, 1, 8)) WHERE user_name IS NULL") // Add NOT NULL constraint DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET NOT NULL") log.Println("user_name column added successfully") } else { // Column exists, update null values log.Println("Updating users with null usernames...") DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(id::text, 1, 8)) WHERE user_name IS NULL OR user_name = ''") // Check if NOT NULL constraint exists using pg_catalog var isNotNull bool DB.Raw(` SELECT attnotnull FROM pg_attribute WHERE attrelid = 'users'::regclass AND attname = 'user_name' `).Scan(&isNotNull) if !isNotNull { log.Println("Adding NOT NULL constraint to user_name...") DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET NOT NULL") } } } func seedRolesAndPermissions() { // 1. Define Permissions permissions := []models.Permission{ {Name: "user:read", Description: "Can read user data"}, {Name: "user:write", Description: "Can modify user data"}, {Name: "admin:access", Description: "Can access admin panel"}, } for _, p := range permissions { DB.FirstOrCreate(&models.Permission{}, models.Permission{Name: p.Name, Description: p.Description}) } // 2. Define Roles roles := []string{"admin", "user"} for _, r := range roles { DB.FirstOrCreate(&models.Role{}, models.Role{Name: r, Description: "Default " + r + " role"}) } // 3. Assign Permissions to Admin Role var adminRole models.Role DB.Preload("Permissions").Where("name = ?", "admin").First(&adminRole) // Fetch all permissions to assign to admin var allPermissions []models.Permission DB.Find(&allPermissions) // Update association (append missing ones) DB.Model(&adminRole).Association("Permissions").Replace(allPermissions) // 4. Assign Basic Permissions to User Role var userRole models.Role DB.Preload("Permissions").Where("name = ?", "user").First(&userRole) var userPermissions []models.Permission DB.Where("name IN ?", []string{"user:read"}).Find(&userPermissions) DB.Model(&userRole).Association("Permissions").Replace(userPermissions) log.Println("Roles and Permissions seeded") } func seedDefaultSettings() { // Seed default CORS whitelist var whitelistCount int64 DB.Model(&models.CorsWhitelist{}).Count(&whitelistCount) if whitelistCount == 0 { defaultWhitelist := []models.CorsWhitelist{ { Origin: "http://localhost:3000", Description: "Default local frontend", IsActive: true, CreatedBy: "system", }, { Origin: "http://localhost:8080", Description: "Backend self", IsActive: true, CreatedBy: "system", }, } for _, w := range defaultWhitelist { DB.Create(&w) } log.Println("Default CORS whitelist seeded") } // Seed default rate limit settings var rateLimitCount int64 DB.Model(&models.RateLimitSetting{}).Count(&rateLimitCount) if rateLimitCount == 0 { defaultRateLimits := []models.RateLimitSetting{ { Name: "login", Description: "Login endpoint rate limit", MaxRequests: 5, WindowSeconds: 60, // 1 minute IsActive: true, }, { Name: "register", Description: "Registration endpoint rate limit", MaxRequests: 3, WindowSeconds: 300, // 5 minutes IsActive: true, }, { Name: "api", Description: "General API rate limit", MaxRequests: 100, WindowSeconds: 60, // 1 minute IsActive: true, }, } for _, r := range defaultRateLimits { DB.Create(&r) } log.Println("Default rate limit settings seeded") } } // SeedDefaultAdmin creates the default admin user if it doesn't exist func SeedDefaultAdmin() { // Check if admin user already exists (including soft-deleted) var adminUser models.User err := DB.Unscoped().Where("email = ?", "admin@gauth.local").First(&adminUser).Error if err != nil { // Admin user doesn't exist, create one // Hash default password: "Admin@123" hashedPassword, err := bcrypt.GenerateFromPassword([]byte("Admin@123"), bcrypt.DefaultCost) if err != nil { log.Printf("Failed to hash admin password: %v", err) return } trueBool := true adminUser = models.User{ Email: "admin@gauth.local", UserName: "admin", Password: string(hashedPassword), EmailVerified: &trueBool, } if err := DB.Create(&adminUser).Error; err != nil { log.Printf("Failed to create admin user: %v", err) return } log.Println("✅ Default admin user created:") log.Println(" Email: admin@gauth.local") log.Println(" Password: Admin@123") log.Println(" ⚠️ Please change this password after first login!") } else { // Admin user exists (possibly soft-deleted) if adminUser.DeletedAt.Valid { log.Println("Restoring deleted admin user...") if err := DB.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil { log.Printf("Failed to restore admin user: %v", err) return } } } // Ensure admin role is assigned var adminRole models.Role if err := DB.Where("name = ?", "admin").First(&adminRole).Error; err != nil { log.Printf("Admin role not found: %v", err) return } if err := DB.Model(&adminUser).Association("Roles").Append(&adminRole); err != nil { log.Printf("Failed to assign admin role: %v", err) } }