first commit
This commit is contained in:
31
database/README_migrate.md
Normal file
31
database/README_migrate.md
Normal file
@@ -0,0 +1,31 @@
|
||||
Kısa kullanım
|
||||
|
||||
Bu proje için GORM AutoMigrate helper'ı `Migrate(db *gorm.DB) error` fonksiyonu olarak sağlanmıştır.
|
||||
|
||||
Örnek kullanım (ör. `main.go` içinde):
|
||||
|
||||
```go
|
||||
import (
|
||||
"gobeyhan/config" // DB konfigürasyonunuza göre düzenleyin
|
||||
"gobeyhan/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := config.NewDB() // veya projenizdeki DB bağlantı fonksiyonu
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := database.Migrate(db); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// uygulama başlat
|
||||
}
|
||||
```
|
||||
|
||||
Notlar:
|
||||
- `database/migrate.go` sadece modeller için `AutoMigrate` çağrısını yapar.
|
||||
- Thumbnail oluşturma ve dosya upload işlemleri model hook'larında değil upload handler'larında yapılmalıdır.
|
||||
- Eğer DB seviyesinde benzersiz constraint'ler isterseniz, GORM tag veya migration dosyası ile `uniqueIndex` ekleyin.
|
||||
|
||||
329
database/db.go
Normal file
329
database/db.go
Normal file
@@ -0,0 +1,329 @@
|
||||
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ı...")
|
||||
}
|
||||
26
database/migrate.go
Normal file
26
database/migrate.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gobeyhan/database/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Migrate runs AutoMigrate for all models used in the project.
|
||||
func Migrate(db *gorm.DB) error {
|
||||
// Order can matter due to foreign keys; migrate parents first
|
||||
return db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.SocialAccount{},
|
||||
&models.Role{},
|
||||
&models.Permission{},
|
||||
&models.Category{},
|
||||
&models.Tag{},
|
||||
&models.Post{},
|
||||
&models.CategoryView{},
|
||||
&models.Comment{},
|
||||
&models.CorsWhitelist{},
|
||||
&models.CorsBlacklist{},
|
||||
&models.RateLimitSetting{},
|
||||
)
|
||||
}
|
||||
274
database/models/blog.go
Normal file
274
database/models/blog.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Note: This file maps Django models to GORM models for MySQL.
|
||||
// Image fields are stored as file path strings. Thumbnail generation and image processing
|
||||
// should be handled elsewhere (e.g., during upload) — TODO: integrate with image processing service.
|
||||
|
||||
// Category represents post categories.
|
||||
type Category struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Title string `gorm:"size:254;not null" json:"title"`
|
||||
Keywords string `gorm:"size:254" json:"keywords"`
|
||||
Desc string `gorm:"size:254" json:"description"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
Order int `gorm:"default:1;index" json:"order"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
Image string `gorm:"size:1024" json:"image"`
|
||||
}
|
||||
|
||||
func (Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// BeforeCreate hook to set slug
|
||||
func (c *Category) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook ensures slug exists
|
||||
func (c *Category) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForCategory(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Category{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tags model
|
||||
type Tag struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Tag string `gorm:"size:254;not null" json:"tag"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
}
|
||||
|
||||
func (Tag) TableName() string { return "tags" }
|
||||
|
||||
func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if t.Slug == "" {
|
||||
t.Slug, err = generateUniqueSlugForTag(tx, t.Tag)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tag) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if t.Slug == "" {
|
||||
t.Slug, err = generateUniqueSlugForTag(tx, t.Tag)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForTag(db *gorm.DB, tag string) (string, error) {
|
||||
slug := normalizeSlug(tag)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Tag{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post model
|
||||
type Post struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Title string `gorm:"size:254;not null" json:"title"`
|
||||
UserID *uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Categories []*Category `gorm:"many2many:post_categories;" json:"categories"`
|
||||
Keywords string `gorm:"size:254" json:"keywords"`
|
||||
Tags []*Tag `gorm:"many2many:post_tags;" json:"tags"`
|
||||
Image string `gorm:"size:1024" json:"image"`
|
||||
Thumb string `gorm:"size:1024" json:"thumb"`
|
||||
Video string `gorm:"size:254;default:'none'" json:"video"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
IsFront bool `gorm:"default:true;index" json:"is_front"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Post `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Post `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (Post) TableName() string { return "posts" }
|
||||
|
||||
func (p *Post) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if p.Slug == "" {
|
||||
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note: Thumbnail generation should be handled in the upload flow.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if p.Slug == "" {
|
||||
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForPost(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Post{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CategoryView model
|
||||
type CategoryView struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
CategoryID uint64 `gorm:"type:bigint unsigned;index" json:"category_id"`
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category"`
|
||||
IPAddress string `gorm:"size:45;index" json:"ip_address"`
|
||||
UserAgent string `gorm:"type:text" json:"user_agent"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (CategoryView) TableName() string { return "category_views" }
|
||||
|
||||
// Comment model
|
||||
type Comment struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||
ProductID uint64 `gorm:"type:bigint unsigned;index" json:"product_id"`
|
||||
Product Post `gorm:"foreignKey:ProductID" json:"product"`
|
||||
Title string `gorm:"size:254" json:"title"`
|
||||
Body string `gorm:"type:text" json:"body"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
Slug string `gorm:"size:250;index" json:"slug"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Comment `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Comment `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (Comment) TableName() string { return "comments" }
|
||||
|
||||
func (c *Comment) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForComment(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Comment) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForComment(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForComment(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Comment{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeSlug replaces Turkish characters, lowercases and makes a basic slug.
|
||||
func normalizeSlug(s string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"ı", "i",
|
||||
"İ", "i",
|
||||
"ç", "c",
|
||||
"Ç", "c",
|
||||
"ş", "s",
|
||||
"Ş", "s",
|
||||
"ö", "o",
|
||||
"Ö", "o",
|
||||
"ü", "u",
|
||||
"Ü", "u",
|
||||
" ", "-",
|
||||
)
|
||||
s = replacer.Replace(s)
|
||||
s = strings.ToLower(s)
|
||||
s = strings.TrimSpace(s)
|
||||
// remove multiple dashes
|
||||
for strings.Contains(s, "--") {
|
||||
s = strings.ReplaceAll(s, "--", "-")
|
||||
}
|
||||
// remove extension-like parts
|
||||
s = strings.Trim(s, "-._")
|
||||
// sanitize file-like chars
|
||||
s = filepath.Clean(s)
|
||||
return s
|
||||
}
|
||||
40
database/models/cors_setting.go
Normal file
40
database/models/cors_setting.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CorsWhitelist - CORS için izin verilen origin'ler
|
||||
type CorsWhitelist struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CorsBlacklist - CORS için yasaklanan origin'ler
|
||||
type CorsBlacklist struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Reason string `gorm:"type:text" json:"reason"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// RateLimitSetting - Rate limit ayarları
|
||||
type RateLimitSetting struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
MaxRequests int64 `gorm:"not null" json:"max_requests"` // Max istek sayısı
|
||||
WindowSeconds int `gorm:"not null" json:"window_seconds"` // Zaman penceresi (saniye)
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
UpdatedBy string `gorm:"type:varchar(255)" json:"updated_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
14
database/models/role.go
Normal file
14
database/models/role.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
type Role struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;not null" json:"name"` // admin, user
|
||||
Description string `json:"description"`
|
||||
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions"`
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;not null" json:"name"` // user:read, user:write
|
||||
Description string `json:"description"`
|
||||
}
|
||||
51
database/models/user.go
Normal file
51
database/models/user.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User model structure
|
||||
type User struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
UserName string `json:"username"`
|
||||
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||
Password string `json:"-"` // Password shouldn't be returned in JSON
|
||||
Avatar string `gorm:"type:varchar(500)" json:"avatar,omitempty"` // Avatar URL from OAuth or uploaded
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Email verification: only required for email/password registration; OAuth users are treated as verified
|
||||
// Changed to *bool to handle false values correctly with GORM defaults
|
||||
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
|
||||
EmailVerifyToken string `gorm:"index" json:"-"`
|
||||
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
|
||||
|
||||
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
|
||||
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
|
||||
}
|
||||
|
||||
// Helper to safely get EmailVerified status
|
||||
func (u *User) IsEmailVerified() bool {
|
||||
if u.EmailVerified == nil {
|
||||
return false
|
||||
}
|
||||
return *u.EmailVerified
|
||||
}
|
||||
|
||||
// SocialAccount model structure
|
||||
type SocialAccount struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
|
||||
Provider string `gorm:"not null" json:"provider"` // google, github
|
||||
ProviderID string `gorm:"not null" json:"provider_id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name,omitempty"` // Full name from provider
|
||||
AvatarURL string `json:"avatar_url,omitempty"` // Avatar URL from provider
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Hooks can be added here if needed
|
||||
106
database/redis.go
Normal file
106
database/redis.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gobeyhan/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var RedisClient *redis.Client
|
||||
var ctx = context.Background()
|
||||
|
||||
func ConnectRedis() {
|
||||
redisURL := config.AppConfig.RedisUrl
|
||||
if redisURL == "" {
|
||||
log.Println("Warning: REDIS_URL is not set, continuing without Redis cache")
|
||||
return
|
||||
}
|
||||
|
||||
opt, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to parse Redis URL: %v, continuing without Redis cache", err)
|
||||
return
|
||||
}
|
||||
|
||||
RedisClient = redis.NewClient(opt)
|
||||
|
||||
// Test connection
|
||||
_, err = RedisClient.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
|
||||
RedisClient = nil
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Connected to Redis successfully")
|
||||
}
|
||||
|
||||
// Set stores a key-value pair in Redis with expiration
|
||||
func Set(key string, value interface{}, expiration time.Duration) error {
|
||||
if RedisClient == nil {
|
||||
return nil // Gracefully handle when Redis is not available
|
||||
}
|
||||
return RedisClient.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
// Get retrieves a value from Redis
|
||||
func Get(key string) (string, error) {
|
||||
if RedisClient == nil {
|
||||
return "", redis.Nil // Return Nil error when Redis is not available
|
||||
}
|
||||
return RedisClient.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Delete removes a key from Redis
|
||||
func Delete(key string) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in Redis
|
||||
func Exists(key string) (bool, error) {
|
||||
if RedisClient == nil {
|
||||
return false, nil
|
||||
}
|
||||
count, err := RedisClient.Exists(ctx, key).Result()
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// SetWithJSON stores a JSON-serializable value in Redis
|
||||
func SetEx(key string, value interface{}, seconds int) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Set(ctx, key, value, time.Duration(seconds)*time.Second).Err()
|
||||
}
|
||||
|
||||
// Increment increments a counter in Redis
|
||||
func Increment(key string) (int64, error) {
|
||||
if RedisClient == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return RedisClient.Incr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Expire sets expiration time for a key
|
||||
func Expire(key string, expiration time.Duration) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
// FlushAll clears all keys in the current database
|
||||
func FlushAll() error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
log.Println("🧹 Clearing Redis Cache...")
|
||||
return RedisClient.FlushDB(ctx).Err()
|
||||
}
|
||||
Reference in New Issue
Block a user