first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:33:39 +03:00
commit 4362c3b83f
1991 changed files with 285411 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
package database
import (
configs "ares/config"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB() {
dsn := configs.AppConfig.DBUrl
if dsn == "" {
if configs.Logger != nil {
configs.Logger.Warn(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
}
return
}
if configs.Logger != nil {
configs.Logger.Info("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.Warn), // 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 {
if configs.Logger != nil {
configs.Logger.Sugar().Errorf("MySQL veritabanı bağlantısı kurulamadı: %v", err)
}
return
}
if configs.Logger != nil {
configs.Logger.Info("MySQL veritabanı bağlantısı kuruldu.")
}
DB = db
}

View File

@@ -0,0 +1,46 @@
package database
import (
configs "ares/config"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DBPg *gorm.DB
func ConnectDBPg() {
dsn := configs.AppConfig.DBPGUrl
if dsn == "" {
if configs.Logger != nil {
configs.Logger.Warn(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
}
return
}
if configs.Logger != nil {
configs.Logger.Info("Yapılandırmada DB_URL_PG bulundu, veritabanına bağlanılmaya çalışılıyor...")
}
// GORM için MySQL konfigürasyonu
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn), // 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 {
if configs.Logger != nil {
configs.Logger.Sugar().Errorf("Postgres veritabanı bağlantısı kurulamadı: %v", err)
}
return
}
if configs.Logger != nil {
configs.Logger.Info("Postgres veritabanı bağlantısı kuruldu.")
}
DBPg = db
}

117
database/config/redis_db.go Normal file
View File

@@ -0,0 +1,117 @@
package database
import (
configs "ares/config"
"context"
"github.com/redis/go-redis/v9"
"time"
)
var RedisClient *redis.Client
var RedisOptions *redis.Options
var ctx = context.Background()
func ConnectRedis() {
redisURL := configs.AppConfig.RedisUrl
if redisURL == "" {
if configs.Logger != nil {
configs.Logger.Warn("Warning: REDIS_URL is not set, continuing without Redis cache")
}
return
}
opt, err := redis.ParseURL(redisURL)
if err != nil {
if configs.Logger != nil {
configs.Logger.Sugar().Warnf("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 {
if configs.Logger != nil {
configs.Logger.Sugar().Warnf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
}
RedisClient = nil
RedisOptions = nil
return
}
if configs.Logger != nil {
configs.Logger.Info("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
}
if configs.Logger != nil {
configs.Logger.Info("🧹 Clearing Redis Cache...")
}
return RedisClient.FlushDB(ctx).Err()
}

165
database/migrate/migrate.go Normal file
View File

@@ -0,0 +1,165 @@
package migrate
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"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.RefreshToken{},
&models.Category{},
&models.Tag{},
&models.Post{},
&models.CategoryView{},
&models.Comment{},
&models.ProductCategory{},
&models.ProductTag{},
&models.Product{},
&models.ProductCategoryView{},
&models.ProductComment{},
&models.Cart{},
&models.CartItem{},
); err != nil {
configs.Logger.Sugar().Errorf("AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
configs.Logger.Info("AutoMigrate Yapıldı.")
} else {
configs.Logger.Info("DB not initialized: skipping AutoMigrate")
}
}
func MigratePg() {
if database.DBPg != nil {
if err := database.DBPg.AutoMigrate(
&models.User{},
&models.SocialAccount{},
&models.Profile{},
&models.Hero{},
&models.Setting{},
&models.CorsWhitelist{},
&models.CorsBlacklist{},
&models.RateLimitSetting{},
&models.RefreshToken{},
&models.Category{},
&models.Tag{},
&models.Post{},
&models.CategoryView{},
&models.Comment{},
&models.ProductCategory{},
&models.ProductTag{},
&models.Product{},
&models.ProductCategoryView{},
&models.ProductComment{},
&models.Cart{},
&models.CartItem{},
); err != nil {
configs.Logger.Sugar().Errorf("PG AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
configs.Logger.Info("PG AutoMigrate Yapıldı.")
} else {
configs.Logger.Info("PG 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)
seedRateLimit("global", "Global endpoint default rate limit", 1000, 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 {
configs.Logger.Sugar().Errorf("RateLimit seed failed (%s): %v", name, err)
return
}
configs.Logger.Sugar().Infof("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 {
configs.Logger.Sugar().Errorf("CorsWhitelist seed failed (%s): %v", origin, err)
return
}
configs.Logger.Sugar().Infof("CorsWhitelist seed created: origin=%s", origin)
}
func defaultWhitelistOrigins() []string {
origins := []string{
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
"http://localhost:8000",
}
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
}

53
database/models/blog.go Normal file
View File

@@ -0,0 +1,53 @@
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" form:"title"`
Images string `gorm:"type:text;not null" json:"images" form:"images"`
ImagesMid string `gorm:"type:text;not null" json:"images_mid" form:"images_mid"`
ImagesMin string `gorm:"type:text;not null" json:"images_min" form:"images_min"`
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" default:"avif"`
Content string `gorm:"type:text" json:"content,omitempty" form:"content"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug" form:"slug"`
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty" form:"categories"`
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty" form:"tags"`
}
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"`
}

20
database/models/cart.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import (
"gorm.io/gorm"
)
type Cart struct {
gorm.Model
UserID uint `gorm:"not null;index" json:"user_id"`
Items []CartItem `gorm:"foreignKey:CartID" json:"items,omitempty"`
}
type CartItem struct {
gorm.Model
CartID uint `gorm:"not null;index" json:"cart_id"`
Cart *Cart `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:CartID" json:"cart,omitempty"`
ProductID uint `gorm:"not null;index" json:"product_id"`
Product *Product `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:ProductID" json:"product,omitempty"`
Quantity int `gorm:"default:1" json:"quantity"`
}

34
database/models/cors.go Normal file
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,96 @@
package models
// Swagger-friendly (light) structs for documentation only.
// These avoid embedding external types (gorm.Model) so `swag` can parse them.
type CategoryDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Children []CategoryDoc `json:"children,omitempty"`
}
type TagDoc struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type PostDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Content string `json:"content,omitempty"`
Images []string `json:"images,omitempty"`
Categories []CategoryDoc `json:"categories,omitempty"`
Tags []TagDoc `json:"tags,omitempty"`
}
type CommentDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
PostID uint `json:"post_id"`
Body string `json:"body,omitempty"`
}
type CategoryViewDoc struct {
ID uint `json:"id"`
CategoryID uint `json:"category_id"`
IPAddress string `json:"ip_address,omitempty"`
}
type ProductCategoryDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description,omitempty"`
Keywords string `json:"keywords,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Children []ProductCategoryDoc `json:"children,omitempty"`
}
type ProductTagDoc struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type ProductDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Images string `json:"images"`
Price float64 `json:"price"`
Width int `json:"width"`
Height int `json:"height"`
Quality int `json:"quality"`
Format string `json:"format"`
Content string `json:"content,omitempty"`
Slug string `json:"slug"`
Categories []ProductCategoryDoc `json:"categories,omitempty"`
Tags []ProductTagDoc `json:"tags,omitempty"`
}
type CartItemDoc struct {
ID uint `json:"id"`
CartID uint `json:"cart_id"`
ProductID uint `json:"product_id"`
Product ProductDoc `json:"product,omitempty"`
Quantity int `json:"quantity"`
}
type CartDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Items []CartItemDoc `json:"items,omitempty"`
}
type ProductCommentDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
Body string `json:"body"`
}
type ProductCategoryViewDoc struct {
ID uint `json:"id"`
CategoryID uint `json:"category_id"`
IPAddress string `json:"ip_address,omitempty"`
}

23
database/models/hero.go Normal file
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,53 @@
package models
import (
"gorm.io/gorm"
)
// Minimal, temiz GORM modelleri
type ProductCategory 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"`
Keywords string `json:"keywords,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Parent *ProductCategory `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;foreignKey:ParentID" json:"parent,omitempty"`
Children []ProductCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Products []Product `gorm:"many2many:product_product_categories;" json:"products,omitempty"`
}
type ProductTag struct {
gorm.Model
Name string `gorm:"type:varchar(254);not null" json:"name"`
Products []Product `gorm:"many2many:product_product_tags;" json:"products,omitempty"`
}
type Product struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title" form:"title"`
Images string `gorm:"type:text;not null" json:"images" form:"images"`
Price float64 `gorm:"type:decimal(10,2);default:0.0" json:"price" form:"price"`
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);default:avif" json:"format" form:"format"`
Content string `gorm:"type:text" json:"content,omitempty" form:"content"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug" form:"slug"`
Categories []ProductCategory `gorm:"many2many:product_product_categories;" json:"categories,omitempty" form:"product_category"`
Tags []ProductTag `gorm:"many2many:product_product_tags;" json:"tags,omitempty" form:"tags"`
}
type ProductCategoryView struct {
gorm.Model
CategoryID uint `json:"category_id"`
IPAddress string `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
}
type ProductComment struct {
gorm.Model
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
Body string `gorm:"type:text" json:"body,omitempty"`
}

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"
}

27
database/models/token.go Normal file
View File

@@ -0,0 +1,27 @@
package models
import (
"time"
"gorm.io/gorm"
)
// RefreshToken represents a server-side record of issued refresh tokens
// to support rotation, revocation and reuse detection.
type RefreshToken struct {
gorm.Model
UserID uint `gorm:"not null;index" json:"user_id"`
TokenID string `gorm:"type:varchar(128);not null;uniqueIndex" json:"token_id"`
// TokenHash is SHA-256 hex of the refresh token string (64 chars).
// Stored instead of the raw token for security, while still allowing debug/lookup.
TokenHash string `gorm:"type:char(64);index" json:"token_hash"`
// TokenFingerprint is a masked representation (e.g. first6...last4) to help operators
// visually correlate DB rows with logs without storing full token.
TokenFingerprint string `gorm:"type:varchar(32);index" json:"token_fingerprint"`
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
Revoked bool `gorm:"index" json:"revoked"`
ReplacedByTokenID string `gorm:"type:varchar(128)" json:"replaced_by_token_id"`
UserAgent string `gorm:"type:varchar(255)" json:"user_agent"`
IP string `gorm:"type:varchar(64)" json:"ip"`
}

48
database/models/user.go Normal file
View File

@@ -0,0 +1,48 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
gorm.Model
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
}

75
database/seeder/seeder.go Normal file
View File

@@ -0,0 +1,75 @@
package seeder
import (
dbConfig "ares/database/config"
"ares/database/models"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// Seed checks for essential data and creates it if missing
func Seed() {
seedAdmin()
}
func seedAdmin() {
// Include soft-deleted records in lookup
var existing models.User
err := dbConfig.DB.Unscoped().Where("email = ?", "admin@example.com").First(&existing).Error
if err == nil {
// Found a user (could be soft-deleted)
// If soft-deleted, restore it
if existing.DeletedAt.Valid {
// Restore (set deleted_at to NULL) and ensure admin/verified flags
updateErr := dbConfig.DB.Unscoped().Model(&existing).Updates(map[string]interface{}{
"deleted_at": nil,
"is_admin": true,
"email_verified": true,
}).Error
if updateErr != nil {
fmt.Println("Admin restore hatası:", updateErr)
return
}
}
// user exists or restored, nothing more to do
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("Admin seed lookup error:", err)
return
}
// If not found at all, create
password := "password123"
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
isTrue := true
admin := models.User{
UserName: "Admin",
Email: "admin@example.com",
Password: string(hashedPassword),
IsAdmin: &isTrue,
EmailVerified: &isTrue,
}
res := dbConfig.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&admin)
if res.Error != nil {
fmt.Println("Admin seed hatası:", res.Error)
return
}
if res.RowsAffected == 0 {
// Another process likely created it concurrently
fmt.Println("Admin kullanıcı zaten mevcut; seed atlandı.")
return
}
fmt.Println("------------------------------------------------")
fmt.Println("Admin kullanıcısı oluşturuldu:")
fmt.Println("Email: admin@example.com")
fmt.Println("Şifre: password123")
fmt.Println("------------------------------------------------")
}