Files
gobeyhan/database/db.go
Beyhan Oğur f34e54c5a5 first commit
2026-04-26 21:43:40 +03:00

330 lines
9.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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ı...")
}