330 lines
9.1 KiB
Go
330 lines
9.1 KiB
Go
package database
|
||
|
||
import (
|
||
//"fmt"
|
||
"log"
|
||
"strings"
|
||
"time"
|
||
|
||
"gobeyhan/config"
|
||
"gobeyhan/database/models"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/driver/mysql"
|
||
"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(mysql.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Info), // 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
|
||
|
||
// MySQL doesn't require enabling uuid-ossp extension. noop for compatibility
|
||
onEnableUUIDForMySQL()
|
||
}
|
||
|
||
func onEnableUUIDForMySQL() {
|
||
// noop: Postgres-only extension; for MySQL UUID handling is usually done at application level
|
||
log.Println("UUID extension step skipped for MySQL (not required)")
|
||
}
|
||
|
||
func SeedAll() {
|
||
if DB == nil {
|
||
log.Println("DB not initialized: call ConnectDB() before SeedAll")
|
||
return
|
||
}
|
||
|
||
// Run AutoMigrate using the helper in migrate.go
|
||
if err := Migrate(DB); err != nil {
|
||
log.Printf("AutoMigrate failed: %v", err)
|
||
return
|
||
}
|
||
|
||
// Run schema/data migrations
|
||
migrateUserNameColumn()
|
||
migrateEmailVerifiedColumn()
|
||
|
||
// Seed initial data
|
||
seedRolesAndPermissions()
|
||
seedDefaultSettings()
|
||
SeedDefaultAdmin()
|
||
|
||
log.Println("Database migration and seeding complete")
|
||
}
|
||
|
||
func migrateEmailVerifiedColumn() {
|
||
// Check column existence via information_schema for MySQL
|
||
var count int64
|
||
DB.Raw(`
|
||
SELECT COUNT(*)
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'email_verified'
|
||
`).Scan(&count)
|
||
|
||
if count == 0 {
|
||
// Column doesn't exist, nothing to migrate
|
||
return
|
||
}
|
||
|
||
// Only set existing users (created before email verification feature) as verified
|
||
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() {
|
||
// Check column existence via information_schema for MySQL
|
||
var count int64
|
||
DB.Raw(`
|
||
SELECT COUNT(*)
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'user_name'
|
||
`).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(CAST(id AS CHAR), 1, 8)) WHERE user_name IS NULL")
|
||
|
||
// Add NOT NULL constraint
|
||
DB.Exec("ALTER TABLE users MODIFY COLUMN user_name TEXT NOT NULL")
|
||
log.Println("user_name column added successfully")
|
||
return
|
||
}
|
||
|
||
// Column exists, update null or empty values
|
||
log.Println("Updating users with null or empty usernames...")
|
||
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(CAST(id AS CHAR), 1, 8)) WHERE user_name IS NULL OR user_name = ''")
|
||
|
||
// Check if NOT NULL constraint exists using information_schema
|
||
var isNullable string
|
||
DB.Raw(`
|
||
SELECT IS_NULLABLE
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'user_name'
|
||
`).Scan(&isNullable)
|
||
|
||
if strings.ToUpper(isNullable) != "NO" {
|
||
log.Println("Adding NOT NULL constraint to user_name...")
|
||
DB.Exec("ALTER TABLE users MODIFY COLUMN user_name TEXT 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 (replace current set)
|
||
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() {
|
||
if DB == nil {
|
||
log.Println("DB not initialized: call ConnectDB() before seeding")
|
||
return
|
||
}
|
||
|
||
// Use a transaction to ensure atomic create + role assignment
|
||
tx := DB.Begin()
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
tx.Rollback()
|
||
log.Printf("panic during SeedDefaultAdmin: %v", r)
|
||
}
|
||
}()
|
||
|
||
// Check if admin user already exists (including soft-deleted)
|
||
var adminUser models.User
|
||
err := tx.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)
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
trueBool := true
|
||
adminUser = models.User{
|
||
Email: "admin@gauth.local",
|
||
UserName: "admin",
|
||
Password: string(hashedPassword),
|
||
EmailVerified: &trueBool,
|
||
}
|
||
|
||
if err := tx.Create(&adminUser).Error; err != nil {
|
||
log.Printf("Failed to create admin user: %v", err)
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
// Log created admin ID and type for debugging
|
||
log.Printf("Admin created - ID value: %v (type: %T)", adminUser.ID, adminUser.ID)
|
||
|
||
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 := tx.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil {
|
||
log.Printf("Failed to restore admin user: %v", err)
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
}
|
||
|
||
// Log existing admin ID for debugging
|
||
log.Printf("Admin already exists - ID value: %v (type: %T)", adminUser.ID, adminUser.ID)
|
||
}
|
||
|
||
// Ensure admin role is assigned
|
||
var adminRole models.Role
|
||
if err := tx.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||
log.Printf("Admin role not found: %v", err)
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
if err := tx.Model(&adminUser).Association("Roles").Append(&adminRole); err != nil {
|
||
log.Printf("Failed to assign admin role: %v", err)
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
if err := tx.Commit().Error; err != nil {
|
||
log.Printf("Failed to commit admin seed transaction: %v", err)
|
||
return
|
||
}
|
||
|
||
log.Println("Varsayılan Yönetici Yaratıldı...")
|
||
}
|