first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:46:42 +03:00
commit 2a5b661443
202 changed files with 49770 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
package database
import (
configs "goGin/config"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB() {
dsn := configs.AppConfig.DBUrl
if dsn == "" {
log.Println(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
return
}
log.Println("Yapılandırmada DB_URL bulundu, veritabanına bağlanılmaya çalışılıyor...")
// GORM için MySQL konfigürasyonu
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // Info seviyesi (performans etkileyebilir); üretimde Error seviyesine alınabilir
PrepareStmt: true, // PrepareStmt performansını artırmak için
NowFunc: func() time.Time {
return time.Now().UTC()
},
})
if err != nil {
log.Println("MySQL veritabanı bağlantısı kurulamadı:", err)
return
}
log.Println("MySQL bağlantısı Sağlandı.")
DB = db
}

View File

@@ -0,0 +1,108 @@
package database
import (
"context"
"github.com/redis/go-redis/v9"
config "goGin/config"
"log"
"time"
)
var RedisClient *redis.Client
var RedisOptions *redis.Options
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)
RedisOptions = nil
return
}
RedisOptions = opt
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
RedisOptions = nil
return
}
log.Println("Redis Bağlatısı Sağlandı")
}
// 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()
}

View File

@@ -0,0 +1,122 @@
package migrasyon
import (
database "goGin/app/database/config"
"goGin/app/database/models"
configs "goGin/config"
"log"
"net/url"
"strings"
)
// Only run AutoMigrate if DB is initialized
func Migrate() {
if database.DB != nil {
if err := database.DB.AutoMigrate(
&models.User{},
&models.SocialAccount{},
&models.Profile{},
&models.Hero{},
&models.Setting{},
&models.CorsWhitelist{},
&models.CorsBlacklist{},
&models.RateLimitSetting{},
&models.Category{},
&models.Tag{},
&models.Post{},
&models.CategoryView{},
&models.Comment{},
); err != nil {
log.Printf("AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
log.Println("AutoMigrate Yapıldı.")
} else {
log.Println("DB not initialized: skipping AutoMigrate")
}
}
func seedSecurityDefaults() {
seedRateLimit("register", "Register endpoint default rate limit", 5, 60)
seedRateLimit("login", "Login endpoint default rate limit", 10, 60)
for _, origin := range defaultWhitelistOrigins() {
seedCorsWhitelist(origin, "default seeded whitelist")
}
}
func seedRateLimit(name, description string, maxRequests int64, windowSeconds int) {
var existing models.RateLimitSetting
if err := database.DB.Where("name = ?", name).First(&existing).Error; err == nil {
return
}
item := models.RateLimitSetting{
Name: name,
Description: description,
MaxRequests: maxRequests,
WindowSeconds: windowSeconds,
IsActive: true,
UpdatedBy: "seed",
}
if err := database.DB.Create(&item).Error; err != nil {
log.Printf("RateLimit seed failed (%s): %v", name, err)
return
}
log.Printf("RateLimit seed created: name=%s max=%d window=%ds", name, maxRequests, windowSeconds)
}
func seedCorsWhitelist(origin, description string) {
origin = strings.TrimSpace(origin)
if origin == "" {
return
}
var existing models.CorsWhitelist
if err := database.DB.Where("origin = ?", origin).First(&existing).Error; err == nil {
return
}
item := models.CorsWhitelist{
Origin: origin,
Description: description,
IsActive: true,
CreatedBy: "seed",
}
if err := database.DB.Create(&item).Error; err != nil {
log.Printf("CorsWhitelist seed failed (%s): %v", origin, err)
return
}
log.Printf("CorsWhitelist seed created: origin=%s", origin)
}
func defaultWhitelistOrigins() []string {
origins := []string{
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
}
appURL := strings.TrimSpace(configs.AppConfig.AppURL)
if appURL != "" {
if parsed, err := url.Parse(appURL); err == nil && parsed.Scheme != "" && parsed.Host != "" {
origins = append(origins, parsed.Scheme+"://"+parsed.Host)
}
}
uniq := make(map[string]struct{})
out := make([]string, 0, len(origins))
for _, origin := range origins {
origin = strings.TrimSpace(origin)
if origin == "" {
continue
}
if _, ok := uniq[origin]; ok {
continue
}
uniq[origin] = struct{}{}
out = append(out, origin)
}
return out
}

View File

@@ -0,0 +1,51 @@
package models
import (
"gorm.io/gorm"
)
// Minimal, temiz GORM modelleri
type Category struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug"`
Description string `json:"description,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Parent *Category `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;foreignKey:ParentID" json:"parent,omitempty"`
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Posts []Post `gorm:"many2many:post_categories;" json:"posts,omitempty"`
}
type Tag struct {
gorm.Model
Name string `gorm:"type:varchar(254);not null" json:"name"`
Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`
}
type Post struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title"`
Images string `gorm:"type:text;not null" json:"images"`
Content string `gorm:"type:text" json:"content,omitempty"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug"`
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty"`
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty"`
Width int `gorm:"default:0" json:"width" form:"width"`
Height int `gorm:"default:0" json:"height" form:"height"`
Quality int `gorm:"default:0" json:"quality" form:"quality"`
Format string `gorm:"type:varchar(10)" json:"format" form:"format"`
}
type CategoryView struct {
gorm.Model
CategoryID uint `json:"category_id"`
IPAddress string `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
}
type Comment struct {
gorm.Model
UserID uint `json:"user_id"`
PostID uint `json:"post_id"`
Body string `gorm:"type:text" json:"body,omitempty"`
}

View File

@@ -0,0 +1,34 @@
package models
import (
"gorm.io/gorm"
)
// CorsWhitelist - CORS için izin verilen origin'ler
type CorsWhitelist struct {
gorm.Model
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
Description string `gorm:"type:varchar(255)" json:"description"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
}
// CorsBlacklist - CORS için yasaklanan origin'ler
type CorsBlacklist struct {
gorm.Model
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
Reason string `gorm:"type:varchar(255)" json:"reason"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
}
// RateLimitSetting - Rate limit ayarları
type RateLimitSetting struct {
gorm.Model
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
Description string `gorm:"type:varchar(255)" 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"`
}

View File

@@ -0,0 +1,23 @@
package models
import (
"gorm.io/gorm"
)
// Banner model structure
// Represents a banner item with optional thumbnail.
type Hero struct {
gorm.Model
Color string `gorm:"type:varchar(32);not null" json:"color" form:"color"`
Title string `gorm:"type:varchar(254)" json:"title,omitempty" form:"title"`
Text1 string `gorm:"type:varchar(254)" json:"text1,omitempty" form:"text1"`
Text2 string `gorm:"type:varchar(254)" json:"text2,omitempty" form:"text2"`
Text4 string `gorm:"type:varchar(254)" json:"text4,omitempty" form:"text4"`
Text5 string `gorm:"type:varchar(254)" json:"text5,omitempty" form:"text5"`
Image string `gorm:"type:varchar(254)" json:"image" form:"image"`
IsActive bool `gorm:"default:true" json:"is_active" form:"is_active"`
Width int `gorm:"default:0" json:"width" form:"width"`
Height int `gorm:"default:0" json:"height" form:"height"`
Quality int `gorm:"default:0" json:"quality" form:"quality"`
Format string `gorm:"type:varchar(10)" json:"format" form:"format"`
}

View File

@@ -0,0 +1,43 @@
package models
import (
"gorm.io/gorm"
)
// Setting model structure
// Stores site-wide metadata and contact information.
type Setting struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title" form:"title"`
MetaTitle string `gorm:"type:varchar(254);not null" json:"meta_title" form:"meta_title"`
MetaDescription string `gorm:"type:varchar(254);not null" json:"meta_description" form:"meta_description"`
Phone string `gorm:"type:varchar(254);not null" json:"phone" form:"phone"`
URL string `gorm:"type:varchar(254);not null" json:"url" form:"url"`
Email string `gorm:"type:varchar(254);not null" json:"email" form:"email"`
Facebook string `gorm:"type:varchar(254)" json:"facebook,omitempty" form:"facebook"`
X string `gorm:"type:varchar(254)" json:"x,omitempty" form:"x"`
Instagram string `gorm:"type:varchar(254)" json:"instagram,omitempty" form:"instagram"`
Whatsapp string `gorm:"type:varchar(254)" json:"whatsapp,omitempty" form:"whatsapp"`
Pinterest string `gorm:"type:varchar(254)" json:"pinterest,omitempty" form:"pinterest"`
Linkedin string `gorm:"type:varchar(254)" json:"linkedin,omitempty" form:"linkedin"`
Slogan string `gorm:"type:varchar(254)" json:"slogan,omitempty" form:"slogan"`
Address string `gorm:"type:text" json:"address,omitempty" form:"address"`
Copyright string `gorm:"type:varchar(254)" json:"copyright,omitempty" form:"copyright"`
MapEmbed string `gorm:"type:text" json:"map_embed,omitempty" form:"map_embed"`
WLogo string `gorm:"type:text" json:"w_logo,omitempty" form:"w_logo"`
BLogo string `gorm:"type:text" json:"b_logo,omitempty" form:"b_logo"`
IsActive bool `gorm:"default:false" json:"is_active" form:"is_active"`
WWidth int `gorm:"default:0" json:"w_width" form:"w_width"`
WHeight int `gorm:"default:0" json:"w_height" form:"w_height"`
WQuality int `gorm:"default:0" json:"w_quality" form:"w_quality"`
WFormat string `gorm:"type:varchar(10)" json:"w_format" form:"w_format"`
BWidth int `gorm:"default:0" json:"b_width" form:"b_width"`
BHeight int `gorm:"default:0" json:"b_height" form:"b_height"`
BQuality int `gorm:"default:0" json:"b_quality" form:"b_quality"`
BFormat string `gorm:"type:varchar(10)" json:"b_format" form:"b_format"`
}
// TableName overrides the table name used by Setting to `settings`
func (Setting) TableName() string {
return "settings"
}

View File

@@ -0,0 +1,51 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
UserName string `json:"username" gorm:"type:varchar(255)"`
Email string `gorm:"uniqueIndex;not null;type:varchar(255)" json:"email"`
Password string `json:"-" gorm:"type:varchar(255)"` // Password shouldn't be returned in JSON
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
EmailVerifyToken string `gorm:"index;type:varchar(255)" json:"-"`
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
IsAdmin *bool `gorm:"default:false" json:"is_admin"`
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
Profile []Profile `gorm:"foreignKey:UserID" json:"profiles,omitempty"`
}
// Email Veriyf i False Döndürüyor
func (u *User) IsEmailVerified() bool {
if u.EmailVerified == nil {
return false
}
return *u.EmailVerified
}
// SocialAccount model structure
type SocialAccount struct {
gorm.Model
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" gorm:"type:varchar(255)"`
Name string `json:"name,omitempty" gorm:"type:varchar(255)"` // Full name from provider
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
}
type Profile struct {
gorm.Model
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
FirstName string `json:"first_name" gorm:"type:varchar(255)"` // Full name from provider
LastName string `json:"last_name" gorm:"type:varchar(255)"` // Full name from provider
}

119
app/database/seed/seed.go Normal file
View File

@@ -0,0 +1,119 @@
package seed
import (
"goGin/app/database/models"
"log"
dbconfig "goGin/app/database/config"
"golang.org/x/crypto/bcrypt"
)
func SeedDefaultSettings() {
// Seed default CORS whitelist
var whitelistCount int64
dbconfig.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 {
dbconfig.DB.Create(&w)
}
log.Println("Default CORS whitelist seeded")
}
// Seed default rate limit settings
var rateLimitCount int64
dbconfig.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 {
dbconfig.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 := dbconfig.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 := dbconfig.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 := dbconfig.DB.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil {
log.Printf("Failed to restore admin user: %v", err)
return
}
}
}
// Admin rolü eklenmesi kaldırıldı çünkü Role modeli yok
}