first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:35:24 +03:00
commit bbbf76b184
592 changed files with 246870 additions and 0 deletions

327
internal/database/db.go Normal file
View File

@@ -0,0 +1,327 @@
package database
import (
"log"
"time"
"gauth-central/config"
"gauth-central/internal/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/postgres"
"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(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Error), // 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
// Enable UUID extension
enableUUIDExtension()
}
func enableUUIDExtension() {
// Enable uuid-ossp extension for uuid_generate_v4()
err := DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error
if err != nil {
log.Printf("Warning: Could not enable uuid-ossp extension: %v", err)
} else {
log.Println("UUID extension enabled")
}
}
func Migrate() {
// Manual migration for user_name column to handle existing data
migrateUserNameColumn()
err := DB.AutoMigrate(
&models.User{},
&models.SocialAccount{},
&models.Role{},
&models.Permission{},
&models.CorsWhitelist{},
&models.CorsBlacklist{},
&models.RateLimitSetting{},
&models.Contact{},
&models.Tag{}, // Added Tag model
&models.PostCategory{},
&models.PostTag{},
&models.Post{},
&models.PostComment{},
&models.PostCategoryView{},
&models.Home{},
&models.About{},
&models.Service{},
&models.ServiceTitle{},
&models.Resume{},
&models.Education{},
&models.Experience{},
&models.Skill{},
&models.Knowledge{},
&models.MainMenu{},
&models.Setting{},
&models.Banner{},
&models.SiteSettings{},
)
if err != nil {
log.Fatal("Database Migration Failed:", err)
}
log.Println("Database Migration Completed")
// Migration for email_verified column is disabled after initial setup
// New users will have email_verified=false by default for email/password registration
// migrateEmailVerifiedColumn()
seedRolesAndPermissions()
seedDefaultSettings()
// seedDefaultAdmin() - Removed from auto migration
}
func migrateEmailVerifiedColumn() {
// Fast check using pg_catalog instead of information_schema
var count int64
DB.Raw(`
SELECT COUNT(*)
FROM pg_attribute
WHERE attrelid = 'users'::regclass
AND attname = 'email_verified'
AND NOT attisdropped
`).Scan(&count)
if count == 0 {
return
}
// Only set existing users (created before email verification feature) as verified
// Users with no verify token AND created before a certain date are old users
// For simplicity: set all users without a verification token as verified (one-time migration)
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() {
// Fast check using pg_catalog instead of information_schema
var count int64
DB.Raw(`
SELECT COUNT(*)
FROM pg_attribute
WHERE attrelid = 'users'::regclass
AND attname = 'user_name'
AND NOT attisdropped
`).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(id::text, 1, 8)) WHERE user_name IS NULL")
// Add NOT NULL constraint
DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET NOT NULL")
log.Println("user_name column added successfully")
} else {
// Column exists, update null values
log.Println("Updating users with null usernames...")
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(id::text, 1, 8)) WHERE user_name IS NULL OR user_name = ''")
// Check if NOT NULL constraint exists using pg_catalog
var isNotNull bool
DB.Raw(`
SELECT attnotnull
FROM pg_attribute
WHERE attrelid = 'users'::regclass
AND attname = 'user_name'
`).Scan(&isNotNull)
if !isNotNull {
log.Println("Adding NOT NULL constraint to user_name...")
DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET 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 (append missing ones)
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() {
// Check if admin user already exists (including soft-deleted)
var adminUser models.User
err := 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 := 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 := DB.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil {
log.Printf("Failed to restore admin user: %v", err)
return
}
}
}
// Ensure admin role is assigned
var adminRole models.Role
if err := DB.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
log.Printf("Admin role not found: %v", err)
return
}
if err := DB.Model(&adminUser).Association("Roles").Append(&adminRole); err != nil {
log.Printf("Failed to assign admin role: %v", err)
}
}

106
internal/database/redis.go Normal file
View File

@@ -0,0 +1,106 @@
package database
import (
"context"
"log"
"time"
"gauth-central/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()
}

View File

@@ -0,0 +1,157 @@
package handlers
import (
"net/http"
"strings"
"gauth-central/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type EducationHandler struct {
educationService *services.EducationService
}
func NewEducationHandler(educationService *services.EducationService) *EducationHandler {
return &EducationHandler{educationService: educationService}
}
func (h *EducationHandler) GetAllEducations(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
// This is public endpoint, returns only active
items, err := h.educationService.GetAllEducations(resumeID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *EducationHandler) AdminGetAllEducations(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
// Admin sees all
items, err := h.educationService.GetAllEducations(resumeID, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *EducationHandler) GetEducationByID(c *gin.Context) {
id := c.Param("id")
item, err := h.educationService.GetEducationByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *EducationHandler) CreateEducation(c *gin.Context) {
var req struct {
BetweenYears string `json:"between_years"`
Title string `json:"title"`
Content string `json:"content"`
ResumeID string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
resumeUUID, err := uuid.Parse(req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
isActive := false
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.educationService.CreateEducation(
req.BetweenYears,
req.Title,
req.Content,
resumeUUID,
isActive,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
func (h *EducationHandler) UpdateEducation(c *gin.Context) {
id := c.Param("id")
var req struct {
BetweenYears *string `json:"between_years"`
Title *string `json:"title"`
Content *string `json:"content"`
ResumeID *string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var resumeUUID *uuid.UUID
if req.ResumeID != nil {
parsed, err := uuid.Parse(*req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
resumeUUID = &parsed
}
item, err := h.educationService.UpdateEducation(
id,
req.BetweenYears,
req.Title,
req.Content,
resumeUUID,
req.IsActive,
)
if err != nil {
status := http.StatusInternalServerError
if err.Error() == "education not found" {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *EducationHandler) DeleteEducation(c *gin.Context) {
id := c.Param("id")
if err := h.educationService.DeleteEducation(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Education deleted successfully"})
}

View File

@@ -0,0 +1,155 @@
package handlers
import (
"net/http"
"strings"
"gauth-central/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type ExperienceHandler struct {
experienceService *services.ExperienceService
}
func NewExperienceHandler(experienceService *services.ExperienceService) *ExperienceHandler {
return &ExperienceHandler{experienceService: experienceService}
}
func (h *ExperienceHandler) GetAllExperiences(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.experienceService.GetAllExperiences(resumeID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *ExperienceHandler) AdminGetAllExperiences(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.experienceService.GetAllExperiences(resumeID, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *ExperienceHandler) GetExperienceByID(c *gin.Context) {
id := c.Param("id")
item, err := h.experienceService.GetExperienceByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *ExperienceHandler) CreateExperience(c *gin.Context) {
var req struct {
BetweenYears string `json:"between_years"`
Title string `json:"title"`
Content string `json:"content"`
ResumeID string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
resumeUUID, err := uuid.Parse(req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
isActive := false
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.experienceService.CreateExperience(
req.BetweenYears,
req.Title,
req.Content,
resumeUUID,
isActive,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
func (h *ExperienceHandler) UpdateExperience(c *gin.Context) {
id := c.Param("id")
var req struct {
BetweenYears *string `json:"between_years"`
Title *string `json:"title"`
Content *string `json:"content"`
ResumeID *string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var resumeUUID *uuid.UUID
if req.ResumeID != nil {
parsed, err := uuid.Parse(*req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
resumeUUID = &parsed
}
item, err := h.experienceService.UpdateExperience(
id,
req.BetweenYears,
req.Title,
req.Content,
resumeUUID,
req.IsActive,
)
if err != nil {
status := http.StatusInternalServerError
if err.Error() == "experience not found" {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *ExperienceHandler) DeleteExperience(c *gin.Context) {
id := c.Param("id")
if err := h.experienceService.DeleteExperience(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Experience deleted successfully"})
}

View File

@@ -0,0 +1,147 @@
package handlers
import (
"net/http"
"strings"
"gauth-central/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type KnowledgeHandler struct {
knowledgeService *services.KnowledgeService
}
func NewKnowledgeHandler(knowledgeService *services.KnowledgeService) *KnowledgeHandler {
return &KnowledgeHandler{knowledgeService: knowledgeService}
}
func (h *KnowledgeHandler) GetAllKnowledges(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.knowledgeService.GetAllKnowledges(resumeID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *KnowledgeHandler) AdminGetAllKnowledges(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.knowledgeService.GetAllKnowledges(resumeID, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *KnowledgeHandler) GetKnowledgeByID(c *gin.Context) {
id := c.Param("id")
item, err := h.knowledgeService.GetKnowledgeByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *KnowledgeHandler) CreateKnowledge(c *gin.Context) {
var req struct {
Title string `json:"title"`
ResumeID string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
resumeUUID, err := uuid.Parse(req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
isActive := false
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.knowledgeService.CreateKnowledge(
req.Title,
resumeUUID,
isActive,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
func (h *KnowledgeHandler) UpdateKnowledge(c *gin.Context) {
id := c.Param("id")
var req struct {
Title *string `json:"title"`
ResumeID *string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var resumeUUID *uuid.UUID
if req.ResumeID != nil {
parsed, err := uuid.Parse(*req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
resumeUUID = &parsed
}
item, err := h.knowledgeService.UpdateKnowledge(
id,
req.Title,
resumeUUID,
req.IsActive,
)
if err != nil {
status := http.StatusInternalServerError
if err.Error() == "knowledge not found" {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *KnowledgeHandler) DeleteKnowledge(c *gin.Context) {
id := c.Param("id")
if err := h.knowledgeService.DeleteKnowledge(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Knowledge deleted successfully"})
}

View File

@@ -0,0 +1,152 @@
package handlers
import (
"net/http"
"strings"
"gauth-central/internal/services"
"github.com/gin-gonic/gin"
)
type ResumeHandler struct {
resumeService *services.ResumeService
}
func NewResumeHandler(resumeService *services.ResumeService) *ResumeHandler {
return &ResumeHandler{resumeService: resumeService}
}
// GetAllResumes retrieves all active resumes.
func (h *ResumeHandler) GetAllResumes(c *gin.Context) {
items, err := h.resumeService.GetAllResumes(true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
// GetActiveResume retrieves the newest active resume.
func (h *ResumeHandler) GetActiveResume(c *gin.Context) {
item, err := h.resumeService.GetFirstActiveResume()
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
// AdminGetAllResumes retrieves all resumes (for admin).
func (h *ResumeHandler) AdminGetAllResumes(c *gin.Context) {
items, err := h.resumeService.GetAllResumes(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
// AdminGetResumeByID retrieves a resume by ID (for admin).
func (h *ResumeHandler) AdminGetResumeByID(c *gin.Context) {
id := c.Param("id")
item, err := h.resumeService.GetResumeByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
// CreateResume creates a new resume.
func (h *ResumeHandler) CreateResume(c *gin.Context) {
var req struct {
Title string `json:"title"`
TitleSub string `json:"title_sub"`
Education string `json:"education"`
Experience string `json:"experience"`
CodingSkills string `json:"coding_skills"`
Knowledge string `json:"knowledge"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
isActive := false
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.resumeService.CreateResume(
req.Title,
req.TitleSub,
req.Education,
req.Experience,
req.CodingSkills,
req.Knowledge,
isActive,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
// UpdateResume updates an existing resume.
func (h *ResumeHandler) UpdateResume(c *gin.Context) {
id := c.Param("id")
var req struct {
Title *string `json:"title"`
TitleSub *string `json:"title_sub"`
Education *string `json:"education"`
Experience *string `json:"experience"`
CodingSkills *string `json:"coding_skills"`
Knowledge *string `json:"knowledge"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item, err := h.resumeService.UpdateResume(
id,
req.Title,
req.TitleSub,
req.Education,
req.Experience,
req.CodingSkills,
req.Knowledge,
req.IsActive,
)
if err != nil {
status := http.StatusInternalServerError
if err.Error() == "resume not found" {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
// DeleteResume deletes a resume.
func (h *ResumeHandler) DeleteResume(c *gin.Context) {
id := c.Param("id")
if err := h.resumeService.DeleteResume(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Resume deleted successfully"})
}

View File

@@ -0,0 +1,151 @@
package handlers
import (
"net/http"
"strings"
"gauth-central/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type SkillHandler struct {
skillService *services.SkillService
}
func NewSkillHandler(skillService *services.SkillService) *SkillHandler {
return &SkillHandler{skillService: skillService}
}
func (h *SkillHandler) GetAllSkills(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.skillService.GetAllSkills(resumeID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *SkillHandler) AdminGetAllSkills(c *gin.Context) {
resumeIDStr := c.Query("resume_id")
var resumeID *string
if resumeIDStr != "" {
resumeID = &resumeIDStr
}
items, err := h.skillService.GetAllSkills(resumeID, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func (h *SkillHandler) GetSkillByID(c *gin.Context) {
id := c.Param("id")
item, err := h.skillService.GetSkillByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *SkillHandler) CreateSkill(c *gin.Context) {
var req struct {
Title string `json:"title"`
Degree int `json:"degree"`
ResumeID string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "title is required"})
return
}
resumeUUID, err := uuid.Parse(req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
isActive := false
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.skillService.CreateSkill(
req.Title,
req.Degree,
resumeUUID,
isActive,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, item)
}
func (h *SkillHandler) UpdateSkill(c *gin.Context) {
id := c.Param("id")
var req struct {
Title *string `json:"title"`
Degree *int `json:"degree"`
ResumeID *string `json:"resume_id"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var resumeUUID *uuid.UUID
if req.ResumeID != nil {
parsed, err := uuid.Parse(*req.ResumeID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid resume_id"})
return
}
resumeUUID = &parsed
}
item, err := h.skillService.UpdateSkill(
id,
req.Title,
req.Degree,
resumeUUID,
req.IsActive,
)
if err != nil {
status := http.StatusInternalServerError
if err.Error() == "skill not found" {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
func (h *SkillHandler) DeleteSkill(c *gin.Context) {
id := c.Param("id")
if err := h.skillService.DeleteSkill(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Skill deleted successfully"})
}

45
internal/models/about.go Normal file
View File

@@ -0,0 +1,45 @@
package models
import (
"time"
"github.com/google/uuid"
)
// About model structure
// Stores public about information with optional media.
type About struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Image string `gorm:"type:text" json:"image,omitempty"`
ImageSub string `gorm:"type:text" json:"image_sub,omitempty"`
CV string `gorm:"type:text" json:"cv,omitempty"`
Birthday string `gorm:"type:varchar(254)" json:"birthday,omitempty"`
City string `gorm:"type:varchar(254)" json:"city,omitempty"`
Study string `gorm:"type:varchar(254)" json:"study,omitempty"`
Website string `gorm:"type:varchar(254)" json:"website,omitempty"`
Phone string `gorm:"type:varchar(254)" json:"phone,omitempty"`
Age string `gorm:"type:varchar(5)" json:"age,omitempty"`
Interests string `gorm:"type:varchar(254)" json:"interests,omitempty"`
Degree string `gorm:"type:varchar(254)" json:"degree,omitempty"`
X string `gorm:"type:varchar(254)" json:"x,omitempty"`
Mail string `gorm:"type:varchar(254)" json:"mail,omitempty"`
Done *int `json:"done,omitempty"`
ProjectDone string `gorm:"type:varchar(254)" json:"project_done,omitempty"`
UserH *int `json:"user_h,omitempty"`
HapyUser string `gorm:"type:varchar(254)" json:"hapy_user,omitempty"`
Great *int `json:"great,omitempty"`
GreatReviews string `gorm:"type:varchar(254)" json:"great_reviews,omitempty"`
Team *int `json:"team,omitempty"`
SupportTeam string `gorm:"type:varchar(254)" json:"support_team,omitempty"`
Slug string `gorm:"type:varchar(250);uniqueIndex;not null" json:"slug"`
IsActive bool `gorm:"default:false" json:"is_active"`
CounterActive bool `gorm:"default:false" json:"counter_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by About to `about`
func (About) TableName() string {
return "about"
}

30
internal/models/banner.go Normal file
View File

@@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Banner model structure
// Represents a banner item with optional thumbnail.
type Banner struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Color string `gorm:"type:varchar(32);not null" json:"color"`
Title string `gorm:"type:varchar(254)" json:"title,omitempty"`
Text1 string `gorm:"type:varchar(254)" json:"text1,omitempty"`
Text2 string `gorm:"type:varchar(254)" json:"text2,omitempty"`
Text4 string `gorm:"type:varchar(254)" json:"text4,omitempty"`
Text5 string `gorm:"type:varchar(254)" json:"text5,omitempty"`
Image string `gorm:"type:text;not null" json:"image"`
ImageK string `gorm:"type:text" json:"image_k,omitempty"`
ImageKTxt string `gorm:"type:varchar(254)" json:"image_k_txt,omitempty"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Banner to `banners`
func (Banner) TableName() string {
return "banners"
}

View File

@@ -0,0 +1,21 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Contact model structure
type Contact struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"not null" json:"email"`
Subject string `gorm:"not null" json:"subject"`
Message string `gorm:"not null" json:"message"`
IP string `json:"ip"`
UserID *uuid.UUID `gorm:"type:uuid" json:"user_id,omitempty"` // Optional: if user is logged in
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,67 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// CorsWhitelist - CORS için izin verilen origin'ler
type CorsWhitelist struct {
ID uuid.UUID `gorm:"type:uuid;primary_key" 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"`
}
// BeforeCreate - UUID otomatik oluşturma
func (c *CorsWhitelist) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
return nil
}
// CorsBlacklist - CORS için yasaklanan origin'ler
type CorsBlacklist struct {
ID uuid.UUID `gorm:"type:uuid;primary_key" 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"`
}
// BeforeCreate - UUID otomatik oluşturma
func (c *CorsBlacklist) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
return nil
}
// RateLimitSetting - Rate limit ayarları
type RateLimitSetting struct {
ID uuid.UUID `gorm:"type:uuid;primary_key" 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"`
}
// BeforeCreate - UUID otomatik oluşturma
func (r *RateLimitSetting) BeforeCreate(tx *gorm.DB) error {
if r.ID == uuid.Nil {
r.ID = uuid.New()
}
return nil
}

View File

@@ -0,0 +1,25 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Education model structure
// Represents a resume education entry.
type Education struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
BetweenYears string `gorm:"type:varchar(50);not null" json:"between_years"`
Title string `gorm:"type:varchar(100);not null" json:"title"`
Content string `gorm:"type:varchar(150);not null" json:"content"`
ResumeID *uuid.UUID `gorm:"type:uuid" json:"resume_id,omitempty"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Education to `educations`
func (Education) TableName() string {
return "educations"
}

View File

@@ -0,0 +1,25 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Experience model structure
// Represents a resume experience entry.
type Experience struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
BetweenYears string `gorm:"type:varchar(50);not null" json:"between_years"`
Title string `gorm:"type:varchar(100);not null" json:"title"`
Content string `gorm:"type:varchar(150);not null" json:"content"`
ResumeID *uuid.UUID `gorm:"type:uuid" json:"resume_id,omitempty"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Experience to `experience`
func (Experience) TableName() string {
return "experience"
}

30
internal/models/home.go Normal file
View File

@@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Home model structure
// Includes a many-to-many relation with tags via home_tags.
type Home struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Name string `gorm:"type:varchar(254);not null" json:"name"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Button1 string `gorm:"type:varchar(254);not null" json:"button1"`
Button2 string `gorm:"type:varchar(254);not null" json:"button2"`
Video string `gorm:"type:text" json:"video,omitempty"`
Keywords string `gorm:"type:varchar(254);not null" json:"keywords"`
Image string `gorm:"type:text" json:"image,omitempty"`
Slug string `gorm:"type:varchar(250);uniqueIndex;not null" json:"slug"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Tags []Tag `gorm:"many2many:home_tags;" json:"tags,omitempty"`
}
// TableName overrides the table name used by Home to `homes`
func (Home) TableName() string {
return "homes"
}

View File

@@ -0,0 +1,23 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Knowledge model structure
// Represents a resume knowledge entry.
type Knowledge struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(100);not null" json:"title"`
ResumeID *uuid.UUID `gorm:"type:uuid" json:"resume_id,omitempty"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Knowledge to `knowledges`
func (Knowledge) TableName() string {
return "knowledges"
}

View File

@@ -0,0 +1,27 @@
package models
import (
"time"
"github.com/google/uuid"
)
// MainMenu model structure
// Stores main menu labels.
type MainMenu struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Home string `gorm:"type:varchar(100);not null" json:"home"`
About string `gorm:"type:varchar(100);not null" json:"about"`
Services string `gorm:"type:varchar(100);not null" json:"services"`
Resume string `gorm:"type:varchar(100);not null" json:"resume"`
Portfolio string `gorm:"type:varchar(100);not null" json:"portfolio"`
Contact string `gorm:"type:varchar(100);not null" json:"contact"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by MainMenu to `mane_menu`
func (MainMenu) TableName() string {
return "mane_menu"
}

33
internal/models/post.go Normal file
View File

@@ -0,0 +1,33 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Post represents a blog post with category and tag relations.
type Post struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Content string `gorm:"type:text" json:"content,omitempty"`
Keywords string `gorm:"type:varchar(254);not null" json:"keywords"`
Image string `gorm:"type:text" json:"image,omitempty"`
Video string `gorm:"type:varchar(254);default:'none'" json:"video,omitempty"`
Slug string `gorm:"type:varchar(250);uniqueIndex;not null" json:"slug"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
IsActive bool `gorm:"default:true" json:"is_active"`
IsFront bool `gorm:"default:true" json:"is_front"`
ParentID *uuid.UUID `json:"parent_id,omitempty"`
Parent *Post `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []Post `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Categories []PostCategory `gorm:"many2many:post_post_categories;" json:"categories,omitempty"`
Tags []PostTag `gorm:"many2many:post_post_tags;" json:"tags,omitempty"`
Comments []PostComment `gorm:"foreignKey:PostID" json:"comments,omitempty"`
}
// TableName overrides the table name used by Post.
func (Post) TableName() string {
return "posts"
}

View File

@@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/google/uuid"
)
// PostCategory represents a blog post category with optional parent-child hierarchy.
type PostCategory struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Keywords string `gorm:"type:varchar(254);not null" json:"keywords"`
Description string `gorm:"type:varchar(254);not null" json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
IsActive bool `gorm:"default:true" json:"is_active"`
Order int `gorm:"default:1;index" json:"order"`
Slug string `gorm:"type:varchar(250);uniqueIndex:idx_post_category_slug_parent;not null" json:"slug"`
ParentID *uuid.UUID `gorm:"uniqueIndex:idx_post_category_slug_parent" json:"parent_id,omitempty"`
Parent *PostCategory `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []PostCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Image string `gorm:"type:text" json:"image,omitempty"`
Posts []Post `gorm:"many2many:post_post_categories;" json:"posts,omitempty"`
}
// TableName overrides the table name used by PostCategory.
func (PostCategory) TableName() string {
return "post_categories"
}

View File

@@ -0,0 +1,22 @@
package models
import (
"time"
"github.com/google/uuid"
)
// PostCategoryView tracks visits to post categories.
type PostCategoryView struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
CategoryID uuid.UUID `gorm:"not null;index:idx_post_category_views,priority:1" json:"category_id"`
Category PostCategory `gorm:"foreignKey:CategoryID" json:"category"`
IPAddress string `gorm:"type:varchar(64);not null;index:idx_post_category_views,priority:2" json:"ip_address"`
UserAgent string `gorm:"type:text" json:"user_agent,omitempty"`
CreatedAt time.Time `gorm:"index:idx_post_category_views,priority:3" json:"created_at"`
}
// TableName overrides the table name used by PostCategoryView.
func (PostCategoryView) TableName() string {
return "post_category_views"
}

View File

@@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/google/uuid"
)
// PostComment represents a comment on a post.
type PostComment struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
UserID uuid.UUID `gorm:"not null" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user"`
PostID uuid.UUID `gorm:"not null" json:"post_id"`
Post Post `gorm:"foreignKey:PostID" json:"post"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Body string `gorm:"type:text;not null" json:"body"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
IsActive bool `gorm:"default:true" json:"is_active"`
Slug string `gorm:"type:varchar(250);uniqueIndex:idx_post_comment_slug_parent;not null" json:"slug"`
ParentID *uuid.UUID `gorm:"uniqueIndex:idx_post_comment_slug_parent" json:"parent_id,omitempty"`
Parent *PostComment `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []PostComment `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
// TableName overrides the table name used by PostComment.
func (PostComment) TableName() string {
return "post_comments"
}

View File

@@ -0,0 +1,22 @@
package models
import (
"time"
"github.com/google/uuid"
)
// PostTag represents tags used for blog posts.
type PostTag struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Tag string `gorm:"type:varchar(254);not null" json:"tag"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
IsActive bool `gorm:"default:true" json:"is_active"`
Posts []Post `gorm:"many2many:post_post_tags;" json:"posts,omitempty"`
}
// TableName overrides the table name used by PostTag.
func (PostTag) TableName() string {
return "post_tags"
}

31
internal/models/resume.go Normal file
View File

@@ -0,0 +1,31 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Resume model structure
// Represents resume section headers.
type Resume struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
TitleSub string `gorm:"type:varchar(254);not null" json:"title_sub"`
Education string `gorm:"type:varchar(100);default:Education" json:"education"`
Experience string `gorm:"type:varchar(100);default:Experience" json:"experience"`
CodingSkills string `gorm:"type:varchar(100);default:Coding Skills" json:"coding_skills"`
Knowledge string `gorm:"type:varchar(100);default:Knowledge" json:"knowledge"`
IsActive bool `gorm:"default:false" json:"is_active"`
Educations []Education `gorm:"foreignKey:ResumeID" json:"educations,omitempty"`
Experiences []Experience `gorm:"foreignKey:ResumeID" json:"experiences,omitempty"`
Skills []Skill `gorm:"foreignKey:ResumeID" json:"skills,omitempty"`
Knowledges []Knowledge `gorm:"foreignKey:ResumeID" json:"knowledges,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Resume to `resumes`
func (Resume) TableName() string {
return "resumes"
}

14
internal/models/role.go Normal file
View File

@@ -0,0 +1,14 @@
package models
type Role struct {
ID uint `gorm:"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 uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null" json:"name"` // user:read, user:write
Description string `json:"description"`
}

View File

@@ -0,0 +1,25 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Service model structure
// Represents a public service item.
type Service struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
Content string `gorm:"type:text" json:"content,omitempty"`
Image string `gorm:"type:text" json:"image,omitempty"`
Slug string `gorm:"type:varchar(250);uniqueIndex;not null" json:"slug"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Service to `services`
func (Service) TableName() string {
return "services"
}

View File

@@ -0,0 +1,23 @@
package models
import (
"time"
"github.com/google/uuid"
)
// ServiceTitle model structure
// Stores title data for services section.
type ServiceTitle struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
TitleSub string `gorm:"type:varchar(254);not null" json:"title_sub"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by ServiceTitle to `services_title`
func (ServiceTitle) TableName() string {
return "services_title"
}

View File

@@ -0,0 +1,39 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Setting model structure
// Stores site-wide metadata and contact information.
type Setting struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(254);not null" json:"title"`
MetaTitle string `gorm:"type:varchar(254);not null" json:"meta_title"`
MetaDescription string `gorm:"type:varchar(254);not null" json:"meta_description"`
Phone string `gorm:"type:varchar(254);not null" json:"phone"`
URL string `gorm:"type:varchar(254);not null" json:"url"`
Email string `gorm:"type:varchar(254);not null" json:"email"`
Facebook string `gorm:"type:varchar(254)" json:"facebook,omitempty"`
X string `gorm:"type:varchar(254)" json:"x,omitempty"`
Instagram string `gorm:"type:varchar(254)" json:"instagram,omitempty"`
Whatsapp string `gorm:"type:varchar(254)" json:"whatsapp,omitempty"`
Pinterest string `gorm:"type:varchar(254)" json:"pinterest,omitempty"`
Linkedin string `gorm:"type:varchar(254)" json:"linkedin,omitempty"`
Slogan string `gorm:"type:varchar(254)" json:"slogan,omitempty"`
Address string `gorm:"type:text" json:"address,omitempty"`
Copyright string `gorm:"type:varchar(254)" json:"copyright,omitempty"`
MapEmbed string `gorm:"type:text" json:"map_embed,omitempty"`
WLogo string `gorm:"type:text" json:"w_logo,omitempty"`
BLogo string `gorm:"type:text" json:"b_logo,omitempty"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Setting to `settings`
func (Setting) TableName() string {
return "settings"
}

View File

@@ -0,0 +1,22 @@
package models
import (
"time"
"github.com/google/uuid"
)
// SiteSettings model structure
// Represents site-wide toggle settings.
type SiteSettings struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
IsActive bool `gorm:"default:true" json:"is_active"`
SiteActive bool `gorm:"default:true" json:"site_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by SiteSettings to `site_settings`
func (SiteSettings) TableName() string {
return "site_settings"
}

24
internal/models/skill.go Normal file
View File

@@ -0,0 +1,24 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Skill model structure
// Represents a resume skill entry.
type Skill struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Title string `gorm:"type:varchar(100);not null" json:"title"`
Degree int `gorm:"not null" json:"degree"`
ResumeID *uuid.UUID `gorm:"type:uuid" json:"resume_id,omitempty"`
IsActive bool `gorm:"default:false" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by Skill to `skills`
func (Skill) TableName() string {
return "skills"
}

21
internal/models/tag.go Normal file
View File

@@ -0,0 +1,21 @@
package models
import (
"time"
"github.com/google/uuid"
)
// Tag model structure
type Tag struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
Tag string `gorm:"type:varchar(254);not null" json:"tag"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName overrides the table name used by User to `tags`
func (Tag) TableName() string {
return "tags"
}

52
internal/models/user.go Normal file
View File

@@ -0,0 +1,52 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// User model structure
type User struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();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,omitempty"`
}
// 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 uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
UserID uuid.UUID `gorm:"not null" 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

View File

@@ -0,0 +1,330 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type AboutService struct{}
func NewAboutService() *AboutService {
return &AboutService{}
}
// CreateAbout creates a new about entry.
func (s *AboutService) CreateAbout(
title string,
image string,
imageSub string,
cv string,
birthday string,
city string,
study string,
website string,
phone string,
age string,
interests string,
degree string,
x string,
mail string,
done *int,
projectDone string,
userH *int,
hapyUser string,
great *int,
greatReviews string,
team *int,
supportTeam string,
isActive bool,
counterActive bool,
) (*models.About, error) {
slug := s.generateUniqueSlug(slugifyAbout(title), "")
about := models.About{
Title: title,
Image: image,
ImageSub: imageSub,
CV: cv,
Birthday: birthday,
City: city,
Study: study,
Website: website,
Phone: phone,
Age: age,
Interests: interests,
Degree: degree,
X: x,
Mail: mail,
Done: done,
ProjectDone: projectDone,
UserH: userH,
HapyUser: hapyUser,
Great: great,
GreatReviews: greatReviews,
Team: team,
SupportTeam: supportTeam,
Slug: slug,
IsActive: isActive,
CounterActive: counterActive,
}
if err := database.DB.Create(&about).Error; err != nil {
return nil, err
}
return s.GetAboutByID(about.ID.String())
}
// GetAllAbout retrieves all about entries. Use onlyActive to filter public data.
func (s *AboutService) GetAllAbout(onlyActive bool) ([]models.About, error) {
var abouts []models.About
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&abouts).Error; err != nil {
return nil, err
}
return abouts, nil
}
// GetFirstActiveAbout returns the newest active about entry.
func (s *AboutService) GetFirstActiveAbout() (*models.About, error) {
var about models.About
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&about).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("about not found")
}
return nil, err
}
return &about, nil
}
// GetAboutByID retrieves an about entry by ID.
func (s *AboutService) GetAboutByID(id string) (*models.About, error) {
var about models.About
if err := database.DB.Where("id = ?", id).First(&about).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("about not found")
}
return nil, err
}
return &about, nil
}
// GetAboutBySlug retrieves an about entry by slug. Use onlyActive to limit public access.
func (s *AboutService) GetAboutBySlug(slug string, onlyActive bool) (*models.About, error) {
var about models.About
query := database.DB.Where("slug = ?", slug)
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.First(&about).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("about not found")
}
return nil, err
}
return &about, nil
}
// UpdateAbout updates an existing about entry.
func (s *AboutService) UpdateAbout(
id string,
title *string,
image *string,
imageSub *string,
cv *string,
birthday *string,
city *string,
study *string,
website *string,
phone *string,
age *string,
interests *string,
degree *string,
x *string,
mail *string,
done *int,
projectDone *string,
userH *int,
hapyUser *string,
great *int,
greatReviews *string,
team *int,
supportTeam *string,
slug *string,
isActive *bool,
counterActive *bool,
) (*models.About, error) {
about, err := s.GetAboutByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if image != nil {
updates["image"] = *image
}
if imageSub != nil {
updates["image_sub"] = *imageSub
}
if cv != nil {
updates["cv"] = *cv
}
if birthday != nil {
updates["birthday"] = *birthday
}
if city != nil {
updates["city"] = *city
}
if study != nil {
updates["study"] = *study
}
if website != nil {
updates["website"] = *website
}
if phone != nil {
updates["phone"] = *phone
}
if age != nil {
updates["age"] = *age
}
if interests != nil {
updates["interests"] = *interests
}
if degree != nil {
updates["degree"] = *degree
}
if x != nil {
updates["x"] = *x
}
if mail != nil {
updates["mail"] = *mail
}
if done != nil {
updates["done"] = *done
}
if projectDone != nil {
updates["project_done"] = *projectDone
}
if userH != nil {
updates["user_h"] = *userH
}
if hapyUser != nil {
updates["hapy_user"] = *hapyUser
}
if great != nil {
updates["great"] = *great
}
if greatReviews != nil {
updates["great_reviews"] = *greatReviews
}
if team != nil {
updates["team"] = *team
}
if supportTeam != nil {
updates["support_team"] = *supportTeam
}
if slug != nil {
clean := slugifyAbout(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if counterActive != nil {
updates["counter_active"] = *counterActive
}
if len(updates) > 0 {
if err := database.DB.Model(about).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetAboutByID(id)
}
// DeleteAbout deletes an about entry by ID.
func (s *AboutService) DeleteAbout(id string) error {
result := database.DB.Delete(&models.About{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("about not found")
}
return nil
}
func (s *AboutService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *AboutService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.About{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugifyAbout(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "about"
}
return clean
}

View File

@@ -0,0 +1,240 @@
package services
import (
"errors"
"time"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gauth-central/pkg/utils"
"github.com/markbates/goth"
"gorm.io/gorm"
)
type AuthService struct {
jwtService *JWTService
}
func NewAuthService() *AuthService {
return &AuthService{
jwtService: NewJWTService(),
}
}
func (s *AuthService) Register(username, email, password string) (*models.User, string, string, string, error) {
// Check if user exists (including soft-deleted users)
var count int64
database.DB.Model(&models.User{}).Unscoped().Where("email = ?", email).Count(&count)
if count > 0 {
return nil, "", "", "", errors.New("email already registered")
}
hashedPassword, err := utils.HashPassword(password)
if err != nil {
return nil, "", "", "", err
}
verifyToken, err := utils.GenerateSecureToken(32)
if err != nil {
return nil, "", "", "", err
}
// Email/password users must verify email; tokens are not issued until verified
falseBool := false
user := models.User{
UserName: username,
Email: email,
Password: hashedPassword,
EmailVerified: &falseBool, // Explicitly set to false
EmailVerifyToken: verifyToken,
}
// Create user - EmailVerified will be false by default or explicitly set
if err := database.DB.Create(&user).Error; err != nil {
// Fallback check for duplicate key error just in case race condition
if utils.IsDuplicateKeyError(err) {
return nil, "", "", "", errors.New("email already registered")
}
return nil, "", "", "", err
}
// Assign default "user" role
var userRole models.Role
if err := database.DB.Where("name = ?", "user").First(&userRole).Error; err == nil {
database.DB.Model(&user).Association("Roles").Append(&userRole)
}
// Reload user with roles (no JWT until email verified)
database.DB.Preload("Roles.Permissions").First(&user, user.ID)
return &user, "", "", verifyToken, nil
}
func (s *AuthService) Login(email, password string) (*models.User, string, string, error) {
var user models.User
// Preload Roles and Permissions
if err := database.DB.Preload("Roles.Permissions").Where("email = ?", email).First(&user).Error; err != nil {
return nil, "", "", errors.New("invalid credentials")
}
if !utils.CheckPasswordHash(password, user.Password) {
return nil, "", "", errors.New("invalid credentials")
}
if !user.IsEmailVerified() {
return nil, "", "", errors.New("email not verified")
}
accessToken, refreshToken, err := s.jwtService.GenerateTokenPair(user)
if err != nil {
return nil, "", "", err
}
return &user, accessToken, refreshToken, nil
}
func (s *AuthService) RefreshToken(refreshToken string) (string, string, error) {
claims, err := s.jwtService.ValidateToken(refreshToken)
if err != nil {
return "", "", err
}
// Here you might want to check against DB if user still exists or is banned
// Also you could implement token rotation (revoke used refresh token)
var user models.User
// Parse UUID from claims and preload permissions
if err := database.DB.Preload("Roles.Permissions").Where("id = ?", claims.UserID).First(&user).Error; err != nil {
return "", "", errors.New("user not found")
}
return s.jwtService.GenerateTokenPair(user)
}
func (s *AuthService) FindOrCreateSocialUser(gothUser goth.User, provider string) (*models.User, string, string, error) {
var socialAccount models.SocialAccount
var user models.User
// Check if social account exists
err := database.DB.Where("provider = ? AND provider_id = ?", provider, gothUser.UserID).First(&socialAccount).Error
if err == nil {
// Social account exists, get user
if err := database.DB.Preload("Roles.Permissions").First(&user, socialAccount.UserID).Error; err != nil {
return nil, "", "", err
}
// Update avatar if changed
if gothUser.AvatarURL != "" && user.Avatar != gothUser.AvatarURL {
database.DB.Model(&user).Update("avatar", gothUser.AvatarURL)
user.Avatar = gothUser.AvatarURL
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
// Check if user with same email exists
err := database.DB.Where("email = ?", gothUser.Email).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// Create new user - use name from provider or generate from email
username := gothUser.NickName
if username == "" {
username = gothUser.Name
}
if username == "" {
// Generate username from email (part before @)
for i, c := range gothUser.Email {
if c == '@' {
username = gothUser.Email[:i]
break
}
}
}
// OAuth providers have already verified email; no email verification required
trueBool := true
user = models.User{
UserName: username,
Email: gothUser.Email,
EmailVerified: &trueBool,
Avatar: gothUser.AvatarURL, // Save avatar from OAuth provider
}
if err := database.DB.Create(&user).Error; err != nil {
return nil, "", "", err
}
} else {
// Linking OAuth to existing user: treat as verified and update avatar
updates := map[string]interface{}{
"email_verified": true,
"email_verify_token": "",
}
if gothUser.AvatarURL != "" {
updates["avatar"] = gothUser.AvatarURL
}
database.DB.Model(&user).Updates(updates)
trueBool := true
user.EmailVerified = &trueBool
user.EmailVerifyToken = ""
if gothUser.AvatarURL != "" {
user.Avatar = gothUser.AvatarURL
}
}
// Create social account with avatar and name
socialAccount = models.SocialAccount{
UserID: user.ID,
Provider: provider,
ProviderID: gothUser.UserID,
Email: gothUser.Email,
Name: gothUser.Name,
AvatarURL: gothUser.AvatarURL,
}
if err := database.DB.Create(&socialAccount).Error; err != nil {
return nil, "", "", err
}
// Assign default "user" role
var userRole models.Role
if err := database.DB.Where("name = ?", "user").First(&userRole).Error; err == nil {
database.DB.Model(&user).Association("Roles").Append(&userRole)
}
} else {
return nil, "", "", err
}
// Make sure we have the user with all roles/permissions and social accounts loaded
database.DB.Preload("Roles.Permissions").Preload("SocialAccounts").First(&user, user.ID)
accessToken, refreshToken, err := s.jwtService.GenerateTokenPair(user)
if err != nil {
return nil, "", "", err
}
return &user, accessToken, refreshToken, nil
}
func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
var user models.User
if err := database.DB.Preload("Roles.Permissions").Where("id = ?", userID).First(&user).Error; err != nil {
return nil, errors.New("user not found")
}
return &user, nil
}
// VerifyEmail marks the user as verified when the token matches. Only for email/password registrations.
func (s *AuthService) VerifyEmail(token string) error {
if token == "" {
return errors.New("invalid verification token")
}
var user models.User
if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("invalid or expired verification token")
}
return err
}
now := time.Now()
return database.DB.Model(&user).Updates(map[string]interface{}{
"email_verified": true,
"email_verify_token": "",
"email_verified_at": &now,
}).Error
}

View File

@@ -0,0 +1,172 @@
package services
import (
"errors"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
const defaultBannerColor = "#FFFFFF"
type BannerService struct{}
func NewBannerService() *BannerService {
return &BannerService{}
}
// CreateBanner creates a new banner entry.
func (s *BannerService) CreateBanner(
color string,
title string,
text1 string,
text2 string,
text4 string,
text5 string,
image string,
imageK string,
imageKTxt string,
isActive bool,
) (*models.Banner, error) {
color = strings.TrimSpace(color)
if color == "" {
color = defaultBannerColor
}
banner := models.Banner{
Color: color,
Title: strings.TrimSpace(title),
Text1: strings.TrimSpace(text1),
Text2: strings.TrimSpace(text2),
Text4: strings.TrimSpace(text4),
Text5: strings.TrimSpace(text5),
Image: image,
ImageK: imageK,
ImageKTxt: strings.TrimSpace(imageKTxt),
IsActive: isActive,
}
if err := database.DB.Create(&banner).Error; err != nil {
return nil, err
}
return s.GetBannerByID(banner.ID.String())
}
// GetAllBanners retrieves all banners. Use onlyActive to filter public data.
func (s *BannerService) GetAllBanners(onlyActive bool) ([]models.Banner, error) {
var banners []models.Banner
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&banners).Error; err != nil {
return nil, err
}
return banners, nil
}
// GetFirstActiveBanner returns the newest active banner.
func (s *BannerService) GetFirstActiveBanner() (*models.Banner, error) {
var banner models.Banner
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&banner).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("banner not found")
}
return nil, err
}
return &banner, nil
}
// GetBannerByID retrieves a banner by ID.
func (s *BannerService) GetBannerByID(id string) (*models.Banner, error) {
var banner models.Banner
if err := database.DB.Where("id = ?", id).First(&banner).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("banner not found")
}
return nil, err
}
return &banner, nil
}
// UpdateBanner updates an existing banner entry.
func (s *BannerService) UpdateBanner(
id string,
color *string,
title *string,
text1 *string,
text2 *string,
text4 *string,
text5 *string,
image *string,
imageK *string,
imageKTxt *string,
isActive *bool,
) (*models.Banner, error) {
banner, err := s.GetBannerByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if color != nil {
clean := strings.TrimSpace(*color)
if clean == "" {
clean = defaultBannerColor
}
updates["color"] = clean
}
if title != nil {
updates["title"] = strings.TrimSpace(*title)
}
if text1 != nil {
updates["text1"] = strings.TrimSpace(*text1)
}
if text2 != nil {
updates["text2"] = strings.TrimSpace(*text2)
}
if text4 != nil {
updates["text4"] = strings.TrimSpace(*text4)
}
if text5 != nil {
updates["text5"] = strings.TrimSpace(*text5)
}
if image != nil {
updates["image"] = *image
}
if imageK != nil {
updates["image_k"] = *imageK
}
if imageKTxt != nil {
updates["image_k_txt"] = strings.TrimSpace(*imageKTxt)
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(banner).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetBannerByID(id)
}
// DeleteBanner deletes a banner by ID.
func (s *BannerService) DeleteBanner(id string) error {
result := database.DB.Delete(&models.Banner{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("banner not found")
}
return nil
}

View File

@@ -0,0 +1,204 @@
package services
import (
"encoding/json"
"gauth-central/internal/database"
"gauth-central/internal/models"
"time"
"github.com/redis/go-redis/v9"
)
type CacheService struct{}
func NewCacheService() *CacheService {
return &CacheService{}
}
// User Cache
func (s *CacheService) SetUser(userID string, user *models.User, expiration time.Duration) error {
userData, err := json.Marshal(user)
if err != nil {
return err
}
return database.Set("user:"+userID, userData, expiration)
}
func (s *CacheService) GetUser(userID string) (*models.User, error) {
data, err := database.Get("user:" + userID)
if err != nil {
return nil, err
}
var user models.User
err = json.Unmarshal([]byte(data), &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *CacheService) DeleteUser(userID string) error {
return database.Delete("user:" + userID)
}
// Session Management
func (s *CacheService) SetSession(token string, userID string, expiration time.Duration) error {
return database.Set("session:"+token, userID, expiration)
}
func (s *CacheService) GetSession(token string) (string, error) {
return database.Get("session:" + token)
}
func (s *CacheService) DeleteSession(token string) error {
return database.Delete("session:" + token)
}
// Rate Limiting
func (s *CacheService) IncrementRateLimit(key string, expiration time.Duration) (int64, error) {
count, err := database.Increment("ratelimit:" + key)
if err != nil {
return 0, err
}
// Set expiration only for first increment
if count == 1 {
database.Expire("ratelimit:"+key, expiration)
}
return count, nil
}
func (s *CacheService) GetRateLimit(key string) (string, error) {
return database.Get("ratelimit:" + key)
}
// Token Blacklist (for logout)
func (s *CacheService) BlacklistToken(token string, expiration time.Duration) error {
return database.Set("blacklist:"+token, "1", expiration)
}
func (s *CacheService) IsTokenBlacklisted(token string) (bool, error) {
return database.Exists("blacklist:" + token)
}
// Email Verification Token Cache
func (s *CacheService) SetEmailVerification(email string, token string, expiration time.Duration) error {
return database.Set("email_verify:"+email, token, expiration)
}
func (s *CacheService) GetEmailVerification(email string) (string, error) {
return database.Get("email_verify:" + email)
}
func (s *CacheService) DeleteEmailVerification(email string) error {
return database.Delete("email_verify:" + email)
}
// Password Reset Token Cache
func (s *CacheService) SetPasswordReset(email string, token string, expiration time.Duration) error {
return database.Set("password_reset:"+email, token, expiration)
}
func (s *CacheService) GetPasswordReset(email string) (string, error) {
return database.Get("password_reset:" + email)
}
func (s *CacheService) DeletePasswordReset(email string) error {
return database.Delete("password_reset:" + email)
}
// CORS Whitelist Cache
func (s *CacheService) SetCorsWhitelist(origins []string, expiration time.Duration) error {
data, err := json.Marshal(origins)
if err != nil {
return err
}
return database.Set("cors:whitelist", data, expiration)
}
func (s *CacheService) GetCorsWhitelist() ([]string, error) {
data, err := database.Get("cors:whitelist")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var origins []string
err = json.Unmarshal([]byte(data), &origins)
if err != nil {
return nil, err
}
return origins, nil
}
func (s *CacheService) InvalidateCorsWhitelist() error {
return database.Delete("cors:whitelist")
}
// CORS Blacklist Cache
func (s *CacheService) SetCorsBlacklist(origins []string, expiration time.Duration) error {
data, err := json.Marshal(origins)
if err != nil {
return err
}
return database.Set("cors:blacklist", data, expiration)
}
func (s *CacheService) GetCorsBlacklist() ([]string, error) {
data, err := database.Get("cors:blacklist")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var origins []string
err = json.Unmarshal([]byte(data), &origins)
if err != nil {
return nil, err
}
return origins, nil
}
func (s *CacheService) InvalidateCorsBlacklist() error {
return database.Delete("cors:blacklist")
}
// Rate Limit Settings Cache
func (s *CacheService) SetRateLimitSettings(settings map[string]*models.RateLimitSetting, expiration time.Duration) error {
data, err := json.Marshal(settings)
if err != nil {
return err
}
return database.Set("settings:ratelimit", data, expiration)
}
func (s *CacheService) GetRateLimitSettings() (map[string]*models.RateLimitSetting, error) {
data, err := database.Get("settings:ratelimit")
if err != nil {
if err == redis.Nil {
return nil, nil
}
return nil, err
}
var settings map[string]*models.RateLimitSetting
err = json.Unmarshal([]byte(data), &settings)
if err != nil {
return nil, err
}
return settings, nil
}
func (s *CacheService) InvalidateRateLimitSettings() error {
return database.Delete("settings:ratelimit")
}

View File

@@ -0,0 +1,95 @@
package services
import (
"errors"
"fmt"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gauth-central/pkg/utils"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ContactService struct{}
func NewContactService() *ContactService {
return &ContactService{}
}
func (s *ContactService) CreateContact(name, email, subject, message, ip string, userID *string) (*models.Contact, error) {
var userUUID *uuid.UUID
if userID != nil {
parsedUUID, err := uuid.Parse(*userID)
if err == nil {
userUUID = &parsedUUID
}
}
contact := models.Contact{
Name: name,
Email: email,
Subject: subject,
Message: message,
IP: ip,
UserID: userUUID,
}
if err := database.DB.Create(&contact).Error; err != nil {
return nil, err
}
// Send email asynchronously (like Celery task)
go func() {
// In a real production app, you might want to use a proper task queue here
// For now, we'll just use a goroutine
err := utils.SendContactEmail(contact.Name, contact.Email, contact.Subject, contact.Message, contact.IP)
if err != nil {
fmt.Printf("Failed to send contact email: %v\n", err)
}
}()
return &contact, nil
}
// GetAllContacts retrieves all contact messages with pagination
func (s *ContactService) GetAllContacts(page, limit int) ([]models.Contact, int64, error) {
var contacts []models.Contact
var total int64
offset := (page - 1) * limit
if err := database.DB.Model(&models.Contact{}).Count(&total).Error; err != nil {
return nil, 0, err
}
if err := database.DB.Preload("User").Order("created_at desc").Limit(limit).Offset(offset).Find(&contacts).Error; err != nil {
return nil, 0, err
}
return contacts, total, nil
}
// GetContactByID retrieves a single contact message by ID
func (s *ContactService) GetContactByID(id string) (*models.Contact, error) {
var contact models.Contact
if err := database.DB.Preload("User").Where("id = ?", id).First(&contact).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("contact not found")
}
return nil, err
}
return &contact, nil
}
// DeleteContact deletes a contact message by ID
func (s *ContactService) DeleteContact(id string) error {
result := database.DB.Delete(&models.Contact{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("contact not found")
}
return nil
}

View File

@@ -0,0 +1,103 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type EducationService struct{}
func NewEducationService() *EducationService {
return &EducationService{}
}
func (s *EducationService) GetAllEducations(resumeID *string, onlyActive bool) ([]models.Education, error) {
var items []models.Education
query := database.DB.Order("created_at desc")
if resumeID != nil {
query = query.Where("resume_id = ?", *resumeID)
}
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (s *EducationService) GetEducationByID(id string) (*models.Education, error) {
var item models.Education
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("education not found")
}
return nil, err
}
return &item, nil
}
func (s *EducationService) CreateEducation(betweenYears, title, content string, resumeID *uuid.UUID, isActive bool) (*models.Education, error) {
item := models.Education{
BetweenYears: betweenYears,
Title: title,
Content: content,
ResumeID: resumeID,
IsActive: isActive,
}
if err := database.DB.Create(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (s *EducationService) UpdateEducation(id string, betweenYears, title, content *string, resumeID *uuid.UUID, isActive *bool) (*models.Education, error) {
item, err := s.GetEducationByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if betweenYears != nil {
updates["between_years"] = *betweenYears
}
if title != nil {
updates["title"] = *title
}
if content != nil {
updates["content"] = *content
}
if resumeID != nil {
updates["resume_id"] = *resumeID
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(item).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetEducationByID(id)
}
func (s *EducationService) DeleteEducation(id string) error {
result := database.DB.Delete(&models.Education{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("education not found")
}
return nil
}

View File

@@ -0,0 +1,103 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ExperienceService struct{}
func NewExperienceService() *ExperienceService {
return &ExperienceService{}
}
func (s *ExperienceService) GetAllExperiences(resumeID *string, onlyActive bool) ([]models.Experience, error) {
var items []models.Experience
query := database.DB.Order("created_at desc")
if resumeID != nil {
query = query.Where("resume_id = ?", *resumeID)
}
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (s *ExperienceService) GetExperienceByID(id string) (*models.Experience, error) {
var item models.Experience
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("experience not found")
}
return nil, err
}
return &item, nil
}
func (s *ExperienceService) CreateExperience(betweenYears, title, content string, resumeID *uuid.UUID, isActive bool) (*models.Experience, error) {
item := models.Experience{
BetweenYears: betweenYears,
Title: title,
Content: content,
ResumeID: resumeID,
IsActive: isActive,
}
if err := database.DB.Create(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (s *ExperienceService) UpdateExperience(id string, betweenYears, title, content *string, resumeID *uuid.UUID, isActive *bool) (*models.Experience, error) {
item, err := s.GetExperienceByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if betweenYears != nil {
updates["between_years"] = *betweenYears
}
if title != nil {
updates["title"] = *title
}
if content != nil {
updates["content"] = *content
}
if resumeID != nil {
updates["resume_id"] = *resumeID
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(item).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetExperienceByID(id)
}
func (s *ExperienceService) DeleteExperience(id string) error {
result := database.DB.Delete(&models.Experience{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("experience not found")
}
return nil
}

View File

@@ -0,0 +1,263 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
const defaultHomeVideoURL = "https://www.youtube.com/watch?v=6zM4p_A0ISk"
type HomeService struct{}
func NewHomeService() *HomeService {
return &HomeService{}
}
// CreateHome creates a new home entry with optional tag relations.
func (s *HomeService) CreateHome(
name string,
title string,
button1 string,
button2 string,
video string,
keywords string,
image string,
tagIDs []string,
isActive bool,
) (*models.Home, error) {
if strings.TrimSpace(video) == "" {
video = defaultHomeVideoURL
}
slug := s.generateUniqueSlug(slugify(name), "")
home := models.Home{
Name: name,
Title: title,
Button1: button1,
Button2: button2,
Video: video,
Keywords: keywords,
Image: image,
Slug: slug,
IsActive: isActive,
}
if len(tagIDs) > 0 {
tags, err := s.fetchTagsByIDs(tagIDs)
if err != nil {
return nil, err
}
home.Tags = tags
}
if err := database.DB.Create(&home).Error; err != nil {
return nil, err
}
return s.GetHomeByID(home.ID.String())
}
// GetAllHomes retrieves all homes. Use onlyActive to filter public data.
func (s *HomeService) GetAllHomes(onlyActive bool) ([]models.Home, error) {
var homes []models.Home
query := database.DB.Preload("Tags").Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&homes).Error; err != nil {
return nil, err
}
return homes, nil
}
// GetHomeByID retrieves a home by ID.
func (s *HomeService) GetHomeByID(id string) (*models.Home, error) {
var home models.Home
if err := database.DB.Preload("Tags").Where("id = ?", id).First(&home).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("home not found")
}
return nil, err
}
return &home, nil
}
// GetHomeBySlug retrieves a home by slug. Use onlyActive to limit public access.
func (s *HomeService) GetHomeBySlug(slug string, onlyActive bool) (*models.Home, error) {
var home models.Home
query := database.DB.Preload("Tags").Where("slug = ?", slug)
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.First(&home).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("home not found")
}
return nil, err
}
return &home, nil
}
// UpdateHome updates an existing home entry and its tag relations.
func (s *HomeService) UpdateHome(
id string,
name *string,
title *string,
button1 *string,
button2 *string,
video *string,
keywords *string,
image *string,
slug *string,
tagIDs *[]string,
isActive *bool,
) (*models.Home, error) {
home, err := s.GetHomeByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if name != nil {
updates["name"] = *name
}
if title != nil {
updates["title"] = *title
}
if button1 != nil {
updates["button1"] = *button1
}
if button2 != nil {
updates["button2"] = *button2
}
if video != nil {
updates["video"] = *video
}
if keywords != nil {
updates["keywords"] = *keywords
}
if image != nil {
updates["image"] = *image
}
if slug != nil {
clean := slugify(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(home).Updates(updates).Error; err != nil {
return nil, err
}
}
if tagIDs != nil {
tags, err := s.fetchTagsByIDs(*tagIDs)
if err != nil {
return nil, err
}
if err := database.DB.Model(home).Association("Tags").Replace(tags); err != nil {
return nil, err
}
}
return s.GetHomeByID(id)
}
// DeleteHome deletes a home by ID.
func (s *HomeService) DeleteHome(id string) error {
result := database.DB.Delete(&models.Home{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("home not found")
}
return nil
}
func (s *HomeService) fetchTagsByIDs(tagIDs []string) ([]models.Tag, error) {
var tags []models.Tag
if len(tagIDs) == 0 {
return tags, nil
}
if err := database.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {
return nil, err
}
if len(tags) != len(tagIDs) {
return nil, errors.New("one or more tags not found")
}
return tags, nil
}
func (s *HomeService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *HomeService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.Home{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugify(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "home"
}
return clean
}

View File

@@ -0,0 +1,146 @@
package services
import (
"errors"
"time"
"gauth-central/config"
"gauth-central/internal/models"
"github.com/golang-jwt/jwt/v5"
)
type JWTClaim struct {
UserID string `json:"sub"`
Email string `json:"email"`
Permissions []string `json:"permissions,omitempty"`
jwt.RegisteredClaims
}
type JWTService struct{}
func NewJWTService() *JWTService {
return &JWTService{}
}
func (s *JWTService) GenerateToken(user models.User) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &JWTClaim{
UserID: user.ID.String(),
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWTSecret))
}
func (s *JWTService) GenerateTokenPair(user models.User) (string, string, error) {
// Extract permissions
permissionMap := make(map[string]bool)
for _, role := range user.Roles {
for _, perm := range role.Permissions {
permissionMap[perm.Name] = true
}
}
var permissions []string
for p := range permissionMap {
permissions = append(permissions, p)
}
// Access Token (15 min)
accessTokenExp := time.Now().Add(15 * time.Minute)
accessClaims := &JWTClaim{
UserID: user.ID.String(),
Email: user.Email,
Permissions: permissions,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(accessTokenExp),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
signedAccessToken, err := accessToken.SignedString([]byte(config.AppConfig.JWTSecret))
if err != nil {
return "", "", err
}
// Refresh Token (7 days)
refreshTokenExp := time.Now().Add(7 * 24 * time.Hour)
refreshClaims := &JWTClaim{
UserID: user.ID.String(),
Email: user.Email,
Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshTokenExp),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
signedRefreshToken, err := refreshToken.SignedString([]byte(config.AppConfig.JWTSecret))
if err != nil {
return "", "", err
}
return signedAccessToken, signedRefreshToken, nil
}
func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) {
token, err := jwt.ParseWithClaims(
signedToken,
&JWTClaim{},
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(config.AppConfig.JWTSecret), nil
},
)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*JWTClaim)
if !ok {
return nil, errors.New("could not parse claims")
}
if claims.ExpiresAt.Time.Before(time.Now()) {
return nil, errors.New("token expired")
}
return claims, nil
}
// GenerateVerificationToken generates a JWT token for email verification (24 hours expiry)
func (s *JWTService) GenerateVerificationToken(userID, email string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &JWTClaim{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "gauth-central",
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.AppConfig.JWTSecret))
}
// ValidateVerificationToken validates a verification token and returns user ID and email
func (s *JWTService) ValidateVerificationToken(tokenString string) (string, string, error) {
claims, err := s.ValidateToken(tokenString)
if err != nil {
return "", "", err
}
return claims.UserID, claims.Email, nil
}

View File

@@ -0,0 +1,95 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type KnowledgeService struct{}
func NewKnowledgeService() *KnowledgeService {
return &KnowledgeService{}
}
func (s *KnowledgeService) GetAllKnowledges(resumeID *string, onlyActive bool) ([]models.Knowledge, error) {
var items []models.Knowledge
query := database.DB.Order("created_at desc")
if resumeID != nil {
query = query.Where("resume_id = ?", *resumeID)
}
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (s *KnowledgeService) GetKnowledgeByID(id string) (*models.Knowledge, error) {
var item models.Knowledge
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("knowledge not found")
}
return nil, err
}
return &item, nil
}
func (s *KnowledgeService) CreateKnowledge(title string, resumeID *uuid.UUID, isActive bool) (*models.Knowledge, error) {
item := models.Knowledge{
Title: title,
ResumeID: resumeID,
IsActive: isActive,
}
if err := database.DB.Create(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (s *KnowledgeService) UpdateKnowledge(id string, title *string, resumeID *uuid.UUID, isActive *bool) (*models.Knowledge, error) {
item, err := s.GetKnowledgeByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if resumeID != nil {
updates["resume_id"] = *resumeID
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(item).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetKnowledgeByID(id)
}
func (s *KnowledgeService) DeleteKnowledge(id string) error {
result := database.DB.Delete(&models.Knowledge{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("knowledge not found")
}
return nil
}

View File

@@ -0,0 +1,134 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type MainMenuService struct{}
func NewMainMenuService() *MainMenuService {
return &MainMenuService{}
}
// CreateMainMenu creates a new main menu entry.
func (s *MainMenuService) CreateMainMenu(home string, about string, services string, resume string, portfolio string, contact string, isActive bool) (*models.MainMenu, error) {
item := models.MainMenu{
Home: home,
About: about,
Services: services,
Resume: resume,
Portfolio: portfolio,
Contact: contact,
IsActive: isActive,
}
if err := database.DB.Create(&item).Error; err != nil {
return nil, err
}
return s.GetMainMenuByID(item.ID.String())
}
// GetAllMainMenus retrieves all main menu entries. Use onlyActive to filter public data.
func (s *MainMenuService) GetAllMainMenus(onlyActive bool) ([]models.MainMenu, error) {
var items []models.MainMenu
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
// GetFirstActiveMainMenu returns the newest active main menu entry.
func (s *MainMenuService) GetFirstActiveMainMenu() (*models.MainMenu, error) {
var item models.MainMenu
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("main menu not found")
}
return nil, err
}
return &item, nil
}
// GetMainMenuByID retrieves a main menu entry by ID.
func (s *MainMenuService) GetMainMenuByID(id string) (*models.MainMenu, error) {
var item models.MainMenu
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("main menu not found")
}
return nil, err
}
return &item, nil
}
// UpdateMainMenu updates an existing main menu entry.
func (s *MainMenuService) UpdateMainMenu(
id string,
home *string,
about *string,
services *string,
resume *string,
portfolio *string,
contact *string,
isActive *bool,
) (*models.MainMenu, error) {
item, err := s.GetMainMenuByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if home != nil {
updates["home"] = *home
}
if about != nil {
updates["about"] = *about
}
if services != nil {
updates["services"] = *services
}
if resume != nil {
updates["resume"] = *resume
}
if portfolio != nil {
updates["portfolio"] = *portfolio
}
if contact != nil {
updates["contact"] = *contact
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(item).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetMainMenuByID(id)
}
// DeleteMainMenu deletes a main menu entry by ID.
func (s *MainMenuService) DeleteMainMenu(id string) error {
result := database.DB.Delete(&models.MainMenu{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("main menu not found")
}
return nil
}

View File

@@ -0,0 +1,214 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type PostCategoryService struct{}
func NewPostCategoryService() *PostCategoryService {
return &PostCategoryService{}
}
func (s *PostCategoryService) CreatePostCategory(
title string,
keywords string,
description string,
image string,
order int,
parentID *uuid.UUID,
isActive bool,
) (*models.PostCategory, error) {
slug := s.generateUniqueSlug(slugifyPostCategory(title), "")
if slug == "" {
return nil, errors.New("slug cannot be empty")
}
category := models.PostCategory{
Title: title,
Keywords: keywords,
Description: description,
Image: image,
Order: order,
ParentID: parentID,
Slug: slug,
IsActive: isActive,
}
if err := database.DB.Create(&category).Error; err != nil {
return nil, err
}
return s.GetPostCategoryByID(category.ID.String())
}
func (s *PostCategoryService) GetAllPostCategories(onlyActive bool) ([]models.PostCategory, error) {
var categories []models.PostCategory
query := database.DB.Order("\"order\" asc").Preload("Children").Where("parent_id IS NULL")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&categories).Error; err != nil {
return nil, err
}
return categories, nil
}
func (s *PostCategoryService) GetPostCategoryByID(id string) (*models.PostCategory, error) {
var category models.PostCategory
if err := database.DB.Preload("Children").Where("id = ?", id).First(&category).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post category not found")
}
return nil, err
}
return &category, nil
}
func (s *PostCategoryService) GetPostCategoryBySlug(slug string, onlyActive bool) (*models.PostCategory, error) {
var category models.PostCategory
query := database.DB.Preload("Children").Where("slug = ?", slug)
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.First(&category).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post category not found")
}
return nil, err
}
return &category, nil
}
func (s *PostCategoryService) UpdatePostCategory(
id string,
title *string,
keywords *string,
description *string,
image *string,
order *int,
parentID *uuid.UUID,
parentIDSet bool,
slug *string,
isActive *bool,
) (*models.PostCategory, error) {
category, err := s.GetPostCategoryByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if keywords != nil {
updates["keywords"] = *keywords
}
if description != nil {
updates["description"] = *description
}
if image != nil {
updates["image"] = *image
}
if order != nil {
updates["order"] = *order
}
if parentIDSet {
updates["parent_id"] = parentID
}
if slug != nil {
clean := slugifyPostCategory(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(category).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetPostCategoryByID(id)
}
func (s *PostCategoryService) DeletePostCategory(id string) error {
result := database.DB.Delete(&models.PostCategory{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("post category not found")
}
return nil
}
func (s *PostCategoryService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *PostCategoryService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.PostCategory{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugifyPostCategory(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "category"
}
return clean
}

View File

@@ -0,0 +1,65 @@
package services
import (
"errors"
"time"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type PostCategoryViewService struct{}
func NewPostCategoryViewService() *PostCategoryViewService {
return &PostCategoryViewService{}
}
// TrackView records a category view once per day per IP.
func (s *PostCategoryViewService) TrackView(categoryID string, ipAddress string, userAgent string) (*models.PostCategoryView, error) {
categoryUUID, err := uuid.Parse(categoryID)
if err != nil {
return nil, errors.New("invalid category_id")
}
startOfDay := time.Now().UTC().Truncate(24 * time.Hour)
endOfDay := startOfDay.Add(24 * time.Hour)
var existing models.PostCategoryView
err = database.DB.Where(
"category_id = ? AND ip_address = ? AND created_at >= ? AND created_at < ?",
categoryUUID,
ipAddress,
startOfDay,
endOfDay,
).First(&existing).Error
if err == nil {
return &existing, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
view := models.PostCategoryView{
CategoryID: categoryUUID,
IPAddress: ipAddress,
UserAgent: userAgent,
}
if err := database.DB.Create(&view).Error; err != nil {
return nil, err
}
return &view, nil
}
// GetViewsByCategory returns all views for a category.
func (s *PostCategoryViewService) GetViewsByCategory(categoryID string) ([]models.PostCategoryView, error) {
var views []models.PostCategoryView
if err := database.DB.Where("category_id = ?", categoryID).Order("created_at desc").Find(&views).Error; err != nil {
return nil, err
}
return views, nil
}

View File

@@ -0,0 +1,200 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type PostCommentService struct{}
func NewPostCommentService() *PostCommentService {
return &PostCommentService{}
}
func (s *PostCommentService) CreatePostComment(
userID uuid.UUID,
postID uuid.UUID,
title string,
body string,
parentID *uuid.UUID,
isActive bool,
) (*models.PostComment, error) {
slug := s.generateUniqueSlug(slugifyPostComment(title), "")
if slug == "" {
return nil, errors.New("slug cannot be empty")
}
comment := models.PostComment{
UserID: userID,
PostID: postID,
Title: title,
Body: body,
ParentID: parentID,
Slug: slug,
IsActive: isActive,
}
if err := database.DB.Create(&comment).Error; err != nil {
return nil, err
}
return s.GetPostCommentByID(comment.ID.String())
}
func (s *PostCommentService) GetPostCommentsByPostID(postID string, onlyActive bool) ([]models.PostComment, error) {
var comments []models.PostComment
query := database.DB.Preload("User").Where("post_id = ?", postID).Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&comments).Error; err != nil {
return nil, err
}
return comments, nil
}
func (s *PostCommentService) GetAllPostComments(postID *string, onlyActive bool) ([]models.PostComment, error) {
var comments []models.PostComment
query := database.DB.Preload("User").Order("created_at desc")
if postID != nil {
query = query.Where("post_id = ?", *postID)
}
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&comments).Error; err != nil {
return nil, err
}
return comments, nil
}
func (s *PostCommentService) GetPostCommentByID(id string) (*models.PostComment, error) {
var comment models.PostComment
if err := database.DB.Preload("User").Where("id = ?", id).First(&comment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post comment not found")
}
return nil, err
}
return &comment, nil
}
func (s *PostCommentService) UpdatePostComment(
id string,
title *string,
body *string,
parentID *uuid.UUID,
parentIDSet bool,
slug *string,
isActive *bool,
) (*models.PostComment, error) {
comment, err := s.GetPostCommentByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if body != nil {
updates["body"] = *body
}
if parentIDSet {
updates["parent_id"] = parentID
}
if slug != nil {
clean := slugifyPostComment(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(comment).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetPostCommentByID(id)
}
func (s *PostCommentService) DeletePostComment(id string) error {
result := database.DB.Delete(&models.PostComment{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("post comment not found")
}
return nil
}
func (s *PostCommentService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *PostCommentService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.PostComment{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugifyPostComment(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "comment"
}
return clean
}

View File

@@ -0,0 +1,297 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type PostService struct{}
func NewPostService() *PostService {
return &PostService{}
}
func (s *PostService) CreatePost(
title string,
content string,
keywords string,
image string,
video string,
categoryIDs []string,
tagIDs []string,
parentID *uuid.UUID,
isActive bool,
isFront bool,
) (*models.Post, error) {
slug := s.generateUniqueSlug(slugifyPost(title), "")
if slug == "" {
return nil, errors.New("slug cannot be empty")
}
post := models.Post{
Title: title,
Content: content,
Keywords: keywords,
Image: image,
Video: video,
Slug: slug,
ParentID: parentID,
IsActive: isActive,
IsFront: isFront,
}
if len(categoryIDs) > 0 {
categories, err := s.fetchCategoriesByIDs(categoryIDs)
if err != nil {
return nil, err
}
post.Categories = categories
}
if len(tagIDs) > 0 {
tags, err := s.fetchTagsByIDs(tagIDs)
if err != nil {
return nil, err
}
post.Tags = tags
}
if err := database.DB.Create(&post).Error; err != nil {
return nil, err
}
return s.GetPostByID(post.ID.String())
}
func (s *PostService) GetAllPosts(onlyActive bool, onlyFront bool, page int, limit int) ([]models.Post, int64, error) {
var posts []models.Post
var total int64
query := database.DB.Model(&models.Post{})
if onlyActive {
query = query.Where("is_active = ?", true)
}
if onlyFront {
query = query.Where("is_front = ?", true)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * limit
if err := query.Preload("Categories").Preload("Tags").
Order("created_at desc").
Limit(limit).
Offset(offset).
Find(&posts).Error; err != nil {
return nil, 0, err
}
return posts, total, nil
}
func (s *PostService) GetPostByID(id string) (*models.Post, error) {
var post models.Post
if err := database.DB.Preload("Categories").Preload("Tags").Where("id = ?", id).First(&post).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post not found")
}
return nil, err
}
return &post, nil
}
func (s *PostService) GetPostBySlug(slug string, onlyActive bool) (*models.Post, error) {
var post models.Post
query := database.DB.Preload("Categories").Preload("Tags").Where("slug = ?", slug)
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.First(&post).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post not found")
}
return nil, err
}
return &post, nil
}
func (s *PostService) UpdatePost(
id string,
title *string,
content *string,
keywords *string,
image *string,
video *string,
categoryIDs *[]string,
tagIDs *[]string,
parentID *uuid.UUID,
parentIDSet bool,
slug *string,
isActive *bool,
isFront *bool,
) (*models.Post, error) {
post, err := s.GetPostByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if content != nil {
updates["content"] = *content
}
if keywords != nil {
updates["keywords"] = *keywords
}
if image != nil {
updates["image"] = *image
}
if video != nil {
updates["video"] = *video
}
if parentIDSet {
updates["parent_id"] = parentID
}
if slug != nil {
clean := slugifyPost(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if isFront != nil {
updates["is_front"] = *isFront
}
if len(updates) > 0 {
if err := database.DB.Model(post).Updates(updates).Error; err != nil {
return nil, err
}
}
if categoryIDs != nil {
categories, err := s.fetchCategoriesByIDs(*categoryIDs)
if err != nil {
return nil, err
}
if err := database.DB.Model(post).Association("Categories").Replace(categories); err != nil {
return nil, err
}
}
if tagIDs != nil {
tags, err := s.fetchTagsByIDs(*tagIDs)
if err != nil {
return nil, err
}
if err := database.DB.Model(post).Association("Tags").Replace(tags); err != nil {
return nil, err
}
}
return s.GetPostByID(id)
}
func (s *PostService) DeletePost(id string) error {
result := database.DB.Delete(&models.Post{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("post not found")
}
return nil
}
func (s *PostService) fetchCategoriesByIDs(categoryIDs []string) ([]models.PostCategory, error) {
var categories []models.PostCategory
if err := database.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil {
return nil, err
}
if len(categories) != len(categoryIDs) {
return nil, errors.New("one or more categories not found")
}
return categories, nil
}
func (s *PostService) fetchTagsByIDs(tagIDs []string) ([]models.PostTag, error) {
var tags []models.PostTag
if err := database.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {
return nil, err
}
if len(tags) != len(tagIDs) {
return nil, errors.New("one or more tags not found")
}
return tags, nil
}
func (s *PostService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *PostService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.Post{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugifyPost(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "post"
}
return clean
}

View File

@@ -0,0 +1,87 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type PostTagService struct{}
func NewPostTagService() *PostTagService {
return &PostTagService{}
}
func (s *PostTagService) CreatePostTag(tagName string, isActive bool) (*models.PostTag, error) {
tag := models.PostTag{
Tag: tagName,
IsActive: isActive,
}
if err := database.DB.Create(&tag).Error; err != nil {
return nil, err
}
return &tag, nil
}
func (s *PostTagService) GetAllPostTags(onlyActive bool) ([]models.PostTag, error) {
var tags []models.PostTag
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&tags).Error; err != nil {
return nil, err
}
return tags, nil
}
func (s *PostTagService) GetPostTagByID(id string) (*models.PostTag, error) {
var tag models.PostTag
if err := database.DB.Where("id = ?", id).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("post tag not found")
}
return nil, err
}
return &tag, nil
}
func (s *PostTagService) UpdatePostTag(id string, tagName string, isActive *bool) (*models.PostTag, error) {
tag, err := s.GetPostTagByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if tagName != "" {
updates["tag"] = tagName
}
if isActive != nil {
updates["is_active"] = *isActive
}
if err := database.DB.Model(tag).Updates(updates).Error; err != nil {
return nil, err
}
return tag, nil
}
func (s *PostTagService) DeletePostTag(id string) error {
result := database.DB.Delete(&models.PostTag{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("post tag not found")
}
return nil
}

View File

@@ -0,0 +1,184 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type ResumeService struct{}
func NewResumeService() *ResumeService {
return &ResumeService{}
}
// CreateResume creates a new resume configuration.
func (s *ResumeService) CreateResume(title, titleSub, education, experience, codingSkills, knowledge string, isActive bool) (*models.Resume, error) {
item := models.Resume{
Title: title,
TitleSub: titleSub,
Education: education,
Experience: experience,
CodingSkills: codingSkills,
Knowledge: knowledge,
IsActive: isActive,
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
if isActive {
// Deactivate all other resumes
if err := tx.Model(&models.Resume{}).Where("is_active = ?", true).Update("is_active", false).Error; err != nil {
return err
}
}
if err := tx.Create(&item).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return s.GetResumeByID(item.ID.String())
}
// GetAllResumes retrieves all resumes. Use onlyActive to filter public data.
// For admin (onlyActive=false), it returns basic info.
// For public (onlyActive=true), it preloads all related data (Educations, Experiences, etc.)
func (s *ResumeService) GetAllResumes(onlyActive bool) ([]models.Resume, error) {
var items []models.Resume
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true).
Preload("Educations", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
}).
Preload("Experiences", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
}).
Preload("Skills", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("degree desc")
}).
Preload("Knowledges", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
})
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
// GetFirstActiveResume returns the single active resume with all relations preloaded.
func (s *ResumeService) GetFirstActiveResume() (*models.Resume, error) {
var item models.Resume
query := database.DB.Where("is_active = ?", true).Order("created_at desc").
Preload("Educations", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
}).
Preload("Experiences", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
}).
Preload("Skills", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("degree desc")
}).
Preload("Knowledges", func(db *gorm.DB) *gorm.DB {
return db.Where("is_active = ?", true).Order("created_at desc")
})
if err := query.First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("resume not found")
}
return nil, err
}
return &item, nil
}
// GetResumeByID retrieves a resume by ID.
func (s *ResumeService) GetResumeByID(id string) (*models.Resume, error) {
var item models.Resume
if err := database.DB.Where("id = ?", id).
Preload("Educations").
Preload("Experiences").
Preload("Skills").
Preload("Knowledges").
First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("resume not found")
}
return nil, err
}
return &item, nil
}
// UpdateResume updates an existing resume entry.
func (s *ResumeService) UpdateResume(id string, title, titleSub, education, experience, codingSkills, knowledge *string, isActive *bool) (*models.Resume, error) {
item, err := s.GetResumeByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if titleSub != nil {
updates["title_sub"] = *titleSub
}
if education != nil {
updates["education"] = *education
}
if experience != nil {
updates["experience"] = *experience
}
if codingSkills != nil {
updates["coding_skills"] = *codingSkills
}
if knowledge != nil {
updates["knowledge"] = *knowledge
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
err := database.DB.Transaction(func(tx *gorm.DB) error {
if isActive != nil && *isActive {
// Deactivate all other resumes except the current one
if err := tx.Model(&models.Resume{}).Where("id != ? AND is_active = ?", id, true).Update("is_active", false).Error; err != nil {
return err
}
}
if err := tx.Model(item).Updates(updates).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
}
return s.GetResumeByID(id)
}
// DeleteResume deletes a resume by ID.
func (s *ResumeService) DeleteResume(id string) error {
result := database.DB.Delete(&models.Resume{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("resume not found")
}
return nil
}

View File

@@ -0,0 +1,193 @@
package services
import (
"errors"
"fmt"
"regexp"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type ServiceService struct{}
func NewServiceService() *ServiceService {
return &ServiceService{}
}
// CreateService creates a new service entry.
func (s *ServiceService) CreateService(title string, content string, image string, isActive bool) (*models.Service, error) {
slug := s.generateUniqueSlug(slugifyService(title), "")
service := models.Service{
Title: title,
Content: content,
Image: image,
Slug: slug,
IsActive: isActive,
}
if err := database.DB.Create(&service).Error; err != nil {
return nil, err
}
return s.GetServiceByID(service.ID.String())
}
// GetAllServices retrieves all services. Use onlyActive to filter public data.
func (s *ServiceService) GetAllServices(onlyActive bool) ([]models.Service, error) {
var services []models.Service
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&services).Error; err != nil {
return nil, err
}
return services, nil
}
// GetServiceByID retrieves a service by ID.
func (s *ServiceService) GetServiceByID(id string) (*models.Service, error) {
var service models.Service
if err := database.DB.Where("id = ?", id).First(&service).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("service not found")
}
return nil, err
}
return &service, nil
}
// GetServiceBySlug retrieves a service by slug. Use onlyActive to limit public access.
func (s *ServiceService) GetServiceBySlug(slug string, onlyActive bool) (*models.Service, error) {
var service models.Service
query := database.DB.Where("slug = ?", slug)
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.First(&service).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("service not found")
}
return nil, err
}
return &service, nil
}
// UpdateService updates an existing service entry.
func (s *ServiceService) UpdateService(
id string,
title *string,
content *string,
image *string,
slug *string,
isActive *bool,
) (*models.Service, error) {
service, err := s.GetServiceByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if content != nil {
updates["content"] = *content
}
if image != nil {
updates["image"] = *image
}
if slug != nil {
clean := slugifyService(*slug)
if clean == "" {
return nil, errors.New("slug cannot be empty")
}
if s.slugExists(clean, id) {
return nil, errors.New("slug already exists")
}
updates["slug"] = clean
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(service).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetServiceByID(id)
}
// DeleteService deletes a service by ID.
func (s *ServiceService) DeleteService(id string) error {
result := database.DB.Delete(&models.Service{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("service not found")
}
return nil
}
func (s *ServiceService) generateUniqueSlug(baseSlug string, excludeID string) string {
slug := baseSlug
counter := 1
for s.slugExists(slug, excludeID) {
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
counter++
}
return slug
}
func (s *ServiceService) slugExists(slug string, excludeID string) bool {
var count int64
query := database.DB.Model(&models.Service{}).Where("slug = ?", slug)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
query.Count(&count)
return count > 0
}
func slugifyService(input string) string {
clean := strings.TrimSpace(input)
if clean == "" {
return ""
}
replacer := strings.NewReplacer(
"ı", "i",
"İ", "i",
"ş", "s",
"Ş", "s",
"ğ", "g",
"Ğ", "g",
"ç", "c",
"Ç", "c",
"ö", "o",
"Ö", "o",
"ü", "u",
"Ü", "u",
)
clean = strings.ToLower(replacer.Replace(clean))
re := regexp.MustCompile(`[^a-z0-9]+`)
clean = re.ReplaceAllString(clean, "-")
clean = strings.Trim(clean, "-")
if clean == "" {
return "service"
}
return clean
}

View File

@@ -0,0 +1,134 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type ServiceTitleService struct{}
func NewServiceTitleService() *ServiceTitleService {
return &ServiceTitleService{}
}
// CreateServiceTitle creates a new service title entry.
func (s *ServiceTitleService) CreateServiceTitle(title string, titleSub string, isActive bool) (*models.ServiceTitle, error) {
item := models.ServiceTitle{
Title: title,
TitleSub: titleSub,
IsActive: isActive,
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
if isActive {
// Deactivate all other service titles
if err := tx.Model(&models.ServiceTitle{}).Where("is_active = ?", true).Update("is_active", false).Error; err != nil {
return err
}
}
if err := tx.Create(&item).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return s.GetServiceTitleByID(item.ID.String())
}
// GetAllServiceTitles retrieves all service titles. Use onlyActive to filter public data.
func (s *ServiceTitleService) GetAllServiceTitles(onlyActive bool) ([]models.ServiceTitle, error) {
var items []models.ServiceTitle
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
// GetFirstActiveServiceTitle returns the newest active service title entry.
func (s *ServiceTitleService) GetFirstActiveServiceTitle() (*models.ServiceTitle, error) {
var item models.ServiceTitle
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("service title not found")
}
return nil, err
}
return &item, nil
}
// GetServiceTitleByID retrieves a service title by ID.
func (s *ServiceTitleService) GetServiceTitleByID(id string) (*models.ServiceTitle, error) {
var item models.ServiceTitle
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("service title not found")
}
return nil, err
}
return &item, nil
}
// UpdateServiceTitle updates an existing service title entry.
func (s *ServiceTitleService) UpdateServiceTitle(id string, title *string, titleSub *string, isActive *bool) (*models.ServiceTitle, error) {
item, err := s.GetServiceTitleByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if titleSub != nil {
updates["title_sub"] = *titleSub
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
err := database.DB.Transaction(func(tx *gorm.DB) error {
if isActive != nil && *isActive {
// Deactivate all other service titles except the current one
if err := tx.Model(&models.ServiceTitle{}).Where("id != ? AND is_active = ?", id, true).Update("is_active", false).Error; err != nil {
return err
}
}
if err := tx.Model(item).Updates(updates).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
}
return s.GetServiceTitleByID(id)
}
// DeleteServiceTitle deletes a service title by ID.
func (s *ServiceTitleService) DeleteServiceTitle(id string) error {
result := database.DB.Delete(&models.ServiceTitle{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("service title not found")
}
return nil
}

View File

@@ -0,0 +1,353 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"log"
"net/url"
"strings"
"time"
"gorm.io/gorm"
)
type SettingsService struct {
cacheService *CacheService
}
func NewSettingsService() *SettingsService {
return &SettingsService{
cacheService: NewCacheService(),
}
}
// ==================== CORS WHITELIST ====================
func (s *SettingsService) GetAllCorsWhitelist() ([]models.CorsWhitelist, error) {
var whitelists []models.CorsWhitelist
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&whitelists).Error
return whitelists, err
}
func (s *SettingsService) GetActiveWhitelistOrigins() ([]string, error) {
// Try cache first
cached, err := s.cacheService.GetCorsWhitelist()
if err == nil && cached != nil {
return cached, nil
}
origins, err := s.getActiveWhitelistOriginsFromDB()
if err != nil {
return nil, err
}
// Cache for 1 hour
s.cacheService.SetCorsWhitelist(origins, 1*time.Hour)
return origins, nil
}
var ErrCorsOriginExists = errors.New("cors origin already exists")
func (s *SettingsService) CreateCorsWhitelist(whitelist *models.CorsWhitelist) error {
var existing models.CorsWhitelist
err := database.DB.Where("LOWER(origin) = LOWER(?)", whitelist.Origin).First(&existing).Error
if err == nil {
if existing.IsActive {
return ErrCorsOriginExists
}
updates := map[string]interface{}{
"is_active": true,
"description": whitelist.Description,
"created_by": whitelist.CreatedBy,
}
err = database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", existing.ID).Updates(updates).Error
if err != nil {
return err
}
s.InvalidateCorsCache()
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = database.DB.Create(whitelist).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) UpdateCorsWhitelist(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) DeleteCorsWhitelist(id string) error {
err := database.DB.Delete(&models.CorsWhitelist{}, "id = ?", id).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
// ==================== CORS BLACKLIST ====================
func (s *SettingsService) GetAllCorsBlacklist() ([]models.CorsBlacklist, error) {
var blacklists []models.CorsBlacklist
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&blacklists).Error
return blacklists, err
}
func (s *SettingsService) GetActiveBlacklistOrigins() ([]string, error) {
// Try cache first
cached, err := s.cacheService.GetCorsBlacklist()
if err == nil && cached != nil {
return cached, nil
}
origins, err := s.getActiveBlacklistOriginsFromDB()
if err != nil {
return nil, err
}
// Cache for 1 hour
s.cacheService.SetCorsBlacklist(origins, 1*time.Hour)
return origins, nil
}
func (s *SettingsService) CreateCorsBlacklist(blacklist *models.CorsBlacklist) error {
err := database.DB.Create(blacklist).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) UpdateCorsBlacklist(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.CorsBlacklist{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
func (s *SettingsService) DeleteCorsBlacklist(id string) error {
err := database.DB.Delete(&models.CorsBlacklist{}, "id = ?", id).Error
if err != nil {
return err
}
// Invalidate cache
s.InvalidateCorsCache()
return nil
}
// ==================== RATE LIMIT SETTINGS ====================
func (s *SettingsService) GetAllRateLimitSettings() ([]models.RateLimitSetting, error) {
var settings []models.RateLimitSetting
err := database.DB.Order("name ASC").Find(&settings).Error
return settings, err
}
func (s *SettingsService) GetRateLimitSettingsMap() (map[string]*models.RateLimitSetting, error) {
// Try cache first
cached, err := s.cacheService.GetRateLimitSettings()
if err == nil && cached != nil {
return cached, nil
}
// Fetch from database
var settings []models.RateLimitSetting
err = database.DB.Where("is_active = ?", true).Find(&settings).Error
if err != nil {
return nil, err
}
settingsMap := make(map[string]*models.RateLimitSetting)
for i := range settings {
settingsMap[settings[i].Name] = &settings[i]
}
// Cache for 1 hour
s.cacheService.SetRateLimitSettings(settingsMap, 1*time.Hour)
return settingsMap, nil
}
func (s *SettingsService) GetRateLimitSettingByName(name string) (*models.RateLimitSetting, error) {
settingsMap, err := s.GetRateLimitSettingsMap()
if err != nil {
return nil, err
}
setting, exists := settingsMap[name]
if !exists {
return nil, nil
}
return setting, nil
}
func (s *SettingsService) UpdateRateLimitSetting(id string, updates map[string]interface{}) error {
err := database.DB.Model(&models.RateLimitSetting{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
return err
}
// Invalidate cache
s.cacheService.InvalidateRateLimitSettings()
return nil
}
// Invalidate CORS caches (whitelist + blacklist)
func (s *SettingsService) InvalidateCorsCache() {
s.cacheService.InvalidateCorsWhitelist()
s.cacheService.InvalidateCorsBlacklist()
log.Println("cors_cache_invalidated")
}
// Check if origin is allowed
func (s *SettingsService) IsOriginAllowed(origin string) (bool, error) {
allowed, _, _, err := s.CheckOrigin(origin)
return allowed, err
}
// CheckOrigin returns decision details for debug logging.
func (s *SettingsService) CheckOrigin(origin string) (bool, string, string, error) {
// Check blacklist first
blacklist, err := s.GetActiveBlacklistOrigins()
if err != nil {
return false, "", "", err
}
for _, blocked := range blacklist {
if originMatchesEntry(origin, blocked) {
return false, blocked, "blacklist", nil
}
}
// Fallback: refresh blacklist on miss (stale cache protection)
freshBlacklist, err := s.getActiveBlacklistOriginsFromDB()
if err != nil {
return false, "", "", err
}
if len(freshBlacklist) != 0 {
s.cacheService.SetCorsBlacklist(freshBlacklist, 1*time.Hour)
}
for _, blocked := range freshBlacklist {
if originMatchesEntry(origin, blocked) {
return false, blocked, "blacklist", nil
}
}
// Check whitelist
whitelist, err := s.GetActiveWhitelistOrigins()
if err != nil {
return false, "", "", err
}
for _, allowed := range whitelist {
if allowed == "*" || originMatchesEntry(origin, allowed) {
return true, allowed, "whitelist", nil
}
}
// Fallback: refresh whitelist on miss (stale cache protection)
freshWhitelist, err := s.getActiveWhitelistOriginsFromDB()
if err != nil {
return false, "", "", err
}
if len(freshWhitelist) != 0 {
s.cacheService.SetCorsWhitelist(freshWhitelist, 1*time.Hour)
}
for _, allowed := range freshWhitelist {
if allowed == "*" || originMatchesEntry(origin, allowed) {
return true, allowed, "whitelist", nil
}
}
return false, "", "whitelist", nil
}
func (s *SettingsService) getActiveWhitelistOriginsFromDB() ([]string, error) {
var whitelists []models.CorsWhitelist
err := database.DB.Where("is_active = ?", true).Find(&whitelists).Error
if err != nil {
return nil, err
}
origins := make([]string, len(whitelists))
for i, w := range whitelists {
origins[i] = w.Origin
}
return origins, nil
}
func (s *SettingsService) getActiveBlacklistOriginsFromDB() ([]string, error) {
var blacklists []models.CorsBlacklist
err := database.DB.Where("is_active = ?", true).Find(&blacklists).Error
if err != nil {
return nil, err
}
origins := make([]string, len(blacklists))
for i, b := range blacklists {
origins[i] = b.Origin
}
return origins, nil
}
func originMatchesEntry(origin string, entry string) bool {
origin = strings.TrimSpace(origin)
entry = strings.TrimSpace(entry)
if origin == "" || entry == "" {
return false
}
originLower := strings.ToLower(origin)
entryLower := strings.ToLower(entry)
if strings.Contains(entryLower, "://") {
return originLower == entryLower
}
parsed, err := url.Parse(originLower)
if err != nil || parsed.Host == "" {
return false
}
hostLower := strings.ToLower(parsed.Host)
if entryLower == hostLower {
return true
}
// Allow entries like "127.0.0.1" to match any port
hostOnly := strings.Split(hostLower, ":")[0]
return entryLower == hostOnly
}

View File

@@ -0,0 +1,245 @@
package services
import (
"errors"
"strings"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
const (
defaultMetaTitle = "Meta Title"
defaultMetaDescription = "Meta Description"
defaultSiteURL = "https://beyhanogur.com.tr"
defaultFacebook = "https://www.facebook.com"
defaultX = "https://www.twitter.com"
defaultInstagram = "https://www.instagram.com"
defaultWhatsapp = "https://www.whatsapp.com"
defaultPinterest = "https://www.pinterest.com"
defaultLinkedin = "https://www.linkedin.com"
defaultSlogan = "Dondurma"
)
type SiteInfoService struct{}
func NewSiteInfoService() *SiteInfoService {
return &SiteInfoService{}
}
// CreateSiteInfo creates a new site info entry.
func (s *SiteInfoService) CreateSiteInfo(
title string,
metaTitle string,
metaDescription string,
phone string,
url string,
email string,
facebook string,
x string,
instagram string,
whatsapp string,
pinterest string,
linkedin string,
slogan string,
wLogo string,
bLogo string,
isActive bool,
address string,
copyright string,
mapEmbed string,
) (*models.Setting, error) {
metaTitle = applyDefault(metaTitle, defaultMetaTitle)
metaDescription = applyDefault(metaDescription, defaultMetaDescription)
url = applyDefault(url, defaultSiteURL)
facebook = applyDefault(facebook, defaultFacebook)
x = applyDefault(x, defaultX)
instagram = applyDefault(instagram, defaultInstagram)
whatsapp = applyDefault(whatsapp, defaultWhatsapp)
pinterest = applyDefault(pinterest, defaultPinterest)
linkedin = applyDefault(linkedin, defaultLinkedin)
slogan = applyDefault(slogan, defaultSlogan)
setting := models.Setting{
Title: title,
MetaTitle: metaTitle,
MetaDescription: metaDescription,
Phone: phone,
URL: url,
Email: email,
Facebook: facebook,
X: x,
Instagram: instagram,
Whatsapp: whatsapp,
Pinterest: pinterest,
Linkedin: linkedin,
Slogan: slogan,
WLogo: wLogo,
BLogo: bLogo,
IsActive: isActive,
Address: address,
Copyright: copyright,
MapEmbed: mapEmbed,
}
if err := database.DB.Create(&setting).Error; err != nil {
return nil, err
}
return s.GetSiteInfoByID(setting.ID.String())
}
// GetAllSiteInfos retrieves all site info entries. Use onlyActive to filter public data.
func (s *SiteInfoService) GetAllSiteInfos(onlyActive bool) ([]models.Setting, error) {
var settings []models.Setting
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&settings).Error; err != nil {
return nil, err
}
return settings, nil
}
// GetFirstActiveSiteInfo returns the newest active site info entry.
func (s *SiteInfoService) GetFirstActiveSiteInfo() (*models.Setting, error) {
var setting models.Setting
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("site info not found")
}
return nil, err
}
return &setting, nil
}
// GetSiteInfoByID retrieves a site info entry by ID.
func (s *SiteInfoService) GetSiteInfoByID(id string) (*models.Setting, error) {
var setting models.Setting
if err := database.DB.Where("id = ?", id).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("site info not found")
}
return nil, err
}
return &setting, nil
}
// UpdateSiteInfo updates an existing site info entry.
func (s *SiteInfoService) UpdateSiteInfo(
id string,
title *string,
metaTitle *string,
metaDescription *string,
phone *string,
url *string,
email *string,
facebook *string,
x *string,
instagram *string,
whatsapp *string,
pinterest *string,
linkedin *string,
slogan *string,
wLogo *string,
bLogo *string,
isActive *bool,
address *string,
copyright *string,
mapEmbed *string,
) (*models.Setting, error) {
setting, err := s.GetSiteInfoByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if metaTitle != nil {
updates["meta_title"] = applyDefault(*metaTitle, defaultMetaTitle)
}
if metaDescription != nil {
updates["meta_description"] = applyDefault(*metaDescription, defaultMetaDescription)
}
if phone != nil {
updates["phone"] = *phone
}
if url != nil {
updates["url"] = applyDefault(*url, defaultSiteURL)
}
if email != nil {
updates["email"] = *email
}
if facebook != nil {
updates["facebook"] = applyDefault(*facebook, defaultFacebook)
}
if x != nil {
updates["x"] = applyDefault(*x, defaultX)
}
if instagram != nil {
updates["instagram"] = applyDefault(*instagram, defaultInstagram)
}
if whatsapp != nil {
updates["whatsapp"] = applyDefault(*whatsapp, defaultWhatsapp)
}
if pinterest != nil {
updates["pinterest"] = applyDefault(*pinterest, defaultPinterest)
}
if linkedin != nil {
updates["linkedin"] = applyDefault(*linkedin, defaultLinkedin)
}
if slogan != nil {
updates["slogan"] = applyDefault(*slogan, defaultSlogan)
}
if wLogo != nil {
updates["w_logo"] = *wLogo
}
if bLogo != nil {
updates["b_logo"] = *bLogo
}
if isActive != nil {
updates["is_active"] = *isActive
}
if address != nil {
updates["address"] = *address
}
if copyright != nil {
updates["copyright"] = *copyright
}
if mapEmbed != nil {
updates["map_embed"] = *mapEmbed
}
if len(updates) > 0 {
if err := database.DB.Model(setting).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetSiteInfoByID(id)
}
// DeleteSiteInfo deletes a site info entry by ID.
func (s *SiteInfoService) DeleteSiteInfo(id string) error {
result := database.DB.Delete(&models.Setting{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("site info not found")
}
return nil
}
func applyDefault(value string, fallback string) string {
clean := strings.TrimSpace(value)
if clean == "" {
return fallback
}
return clean
}

View File

@@ -0,0 +1,109 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type SiteSettingsService struct{}
func NewSiteSettingsService() *SiteSettingsService {
return &SiteSettingsService{}
}
// CreateSiteSettings creates a new site settings entry.
func (s *SiteSettingsService) CreateSiteSettings(isActive bool, siteActive bool) (*models.SiteSettings, error) {
settings := models.SiteSettings{
IsActive: isActive,
SiteActive: siteActive,
}
if err := database.DB.Create(&settings).Error; err != nil {
return nil, err
}
return s.GetSiteSettingsByID(settings.ID.String())
}
// GetAllSiteSettings retrieves all site settings entries. Use onlyActive to filter public data.
func (s *SiteSettingsService) GetAllSiteSettings(onlyActive bool) ([]models.SiteSettings, error) {
var settings []models.SiteSettings
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&settings).Error; err != nil {
return nil, err
}
return settings, nil
}
// GetFirstActiveSiteSettings returns the newest active site settings entry.
func (s *SiteSettingsService) GetFirstActiveSiteSettings() (*models.SiteSettings, error) {
var settings models.SiteSettings
if err := database.DB.Where("is_active = ?", true).Order("created_at desc").First(&settings).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("site settings not found")
}
return nil, err
}
return &settings, nil
}
// GetSiteSettingsByID retrieves a site settings entry by ID.
func (s *SiteSettingsService) GetSiteSettingsByID(id string) (*models.SiteSettings, error) {
var settings models.SiteSettings
if err := database.DB.Where("id = ?", id).First(&settings).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("site settings not found")
}
return nil, err
}
return &settings, nil
}
// UpdateSiteSettings updates an existing site settings entry.
func (s *SiteSettingsService) UpdateSiteSettings(
id string,
isActive *bool,
siteActive *bool,
) (*models.SiteSettings, error) {
settings, err := s.GetSiteSettingsByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if isActive != nil {
updates["is_active"] = *isActive
}
if siteActive != nil {
updates["site_active"] = *siteActive
}
if len(updates) > 0 {
if err := database.DB.Model(settings).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetSiteSettingsByID(id)
}
// DeleteSiteSettings deletes a site settings entry by ID.
func (s *SiteSettingsService) DeleteSiteSettings(id string) error {
result := database.DB.Delete(&models.SiteSettings{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("site settings not found")
}
return nil
}

View File

@@ -0,0 +1,99 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type SkillService struct{}
func NewSkillService() *SkillService {
return &SkillService{}
}
func (s *SkillService) GetAllSkills(resumeID *string, onlyActive bool) ([]models.Skill, error) {
var items []models.Skill
query := database.DB.Order("degree desc")
if resumeID != nil {
query = query.Where("resume_id = ?", *resumeID)
}
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (s *SkillService) GetSkillByID(id string) (*models.Skill, error) {
var item models.Skill
if err := database.DB.Where("id = ?", id).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("skill not found")
}
return nil, err
}
return &item, nil
}
func (s *SkillService) CreateSkill(title string, degree int, resumeID *uuid.UUID, isActive bool) (*models.Skill, error) {
item := models.Skill{
Title: title,
Degree: degree,
ResumeID: resumeID,
IsActive: isActive,
}
if err := database.DB.Create(&item).Error; err != nil {
return nil, err
}
return &item, nil
}
func (s *SkillService) UpdateSkill(id string, title *string, degree *int, resumeID *uuid.UUID, isActive *bool) (*models.Skill, error) {
item, err := s.GetSkillByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if title != nil {
updates["title"] = *title
}
if degree != nil {
updates["degree"] = *degree
}
if resumeID != nil {
updates["resume_id"] = *resumeID
}
if isActive != nil {
updates["is_active"] = *isActive
}
if len(updates) > 0 {
if err := database.DB.Model(item).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetSkillByID(id)
}
func (s *SkillService) DeleteSkill(id string) error {
result := database.DB.Delete(&models.Skill{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("skill not found")
}
return nil
}

View File

@@ -0,0 +1,91 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"gorm.io/gorm"
)
type TagService struct{}
func NewTagService() *TagService {
return &TagService{}
}
// CreateTag creates a new tag
func (s *TagService) CreateTag(tagName string, isActive bool) (*models.Tag, error) {
tag := models.Tag{
Tag: tagName,
IsActive: isActive,
}
if err := database.DB.Create(&tag).Error; err != nil {
return nil, err
}
return &tag, nil
}
// GetAllTags retrieves all tags (optionally filter by active status)
func (s *TagService) GetAllTags(onlyActive bool) ([]models.Tag, error) {
var tags []models.Tag
query := database.DB.Order("created_at desc")
if onlyActive {
query = query.Where("is_active = ?", true)
}
if err := query.Find(&tags).Error; err != nil {
return nil, err
}
return tags, nil
}
// GetTagByID retrieves a tag by ID
func (s *TagService) GetTagByID(id string) (*models.Tag, error) {
var tag models.Tag
if err := database.DB.Where("id = ?", id).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("tag not found")
}
return nil, err
}
return &tag, nil
}
// UpdateTag updates an existing tag
func (s *TagService) UpdateTag(id string, tagName string, isActive *bool) (*models.Tag, error) {
tag, err := s.GetTagByID(id)
if err != nil {
return nil, err
}
updates := map[string]interface{}{}
if tagName != "" {
updates["tag"] = tagName
}
if isActive != nil {
updates["is_active"] = *isActive
}
if err := database.DB.Model(tag).Updates(updates).Error; err != nil {
return nil, err
}
return tag, nil
}
// DeleteTag deletes a tag
func (s *TagService) DeleteTag(id string) error {
result := database.DB.Delete(&models.Tag{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("tag not found")
}
return nil
}

View File

@@ -0,0 +1,257 @@
package services
import (
"errors"
"gauth-central/internal/database"
"gauth-central/internal/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserManagementService struct{}
func NewUserManagementService() *UserManagementService {
return &UserManagementService{}
}
var ErrUserNotFound = errors.New("user not found")
// GetAllUsers - Tüm kullanıcıları getir (admin için)
func (s *UserManagementService) GetAllUsers(page, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
// Count total users
database.DB.Model(&models.User{}).Count(&total)
// Calculate offset
offset := (page - 1) * limit
// Fetch users with pagination and preload roles
err := database.DB.
Preload("Roles").
Preload("SocialAccounts").
Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&users).Error
return users, total, err
}
// GetUserByID - ID'ye göre kullanıcı getir
func (s *UserManagementService) GetUserByID(userID string) (*models.User, error) {
var user models.User
err := database.DB.
Preload("Roles").
Preload("Roles.Permissions").
Preload("SocialAccounts").
Where("id = ?", userID).
First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// CreateUser - Yeni kullanıcı oluştur (admin tarafından)
func (s *UserManagementService) CreateUser(email, password, userName string, emailVerified bool, roleNames []string) (*models.User, error) {
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
Email: email,
UserName: userName,
Password: string(hashedPassword),
EmailVerified: &emailVerified,
}
// Create user
if err := database.DB.Create(user).Error; err != nil {
return nil, err
}
// Assign roles
if len(roleNames) > 0 {
var roles []models.Role
database.DB.Where("name IN ?", roleNames).Find(&roles)
if len(roles) > 0 {
database.DB.Model(user).Association("Roles").Append(roles)
}
} else {
// Assign default "user" role
var userRole models.Role
database.DB.Where("name = ?", "user").First(&userRole)
database.DB.Model(user).Association("Roles").Append(&userRole)
}
// Reload user with roles
database.DB.Preload("Roles").Where("id = ?", user.ID).First(user)
return user, nil
}
// UpdateUser - Kullanıcı bilgilerini güncelle
func (s *UserManagementService) UpdateUser(userID string, updates map[string]interface{}) error {
// Check if user exists
var user models.User
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
return err
}
// If password is being updated, hash it
if password, ok := updates["password"].(string); ok && password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
updates["password"] = string(hashedPassword)
}
// Use Updates with Select to update specific fields including zero values
return database.DB.Model(&user).Updates(updates).Error
}
// DeleteUser - Kullanıcıyı sil (soft delete, hard=true ise kalıcı silme)
func (s *UserManagementService) DeleteUser(userID string, hardDelete bool) error {
if hardDelete {
// Hard delete - ilişkili kayıtları da sil
var user models.User
if err := database.DB.Unscoped().Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrUserNotFound
}
return err
}
// Delete relations first
database.DB.Exec("DELETE FROM user_roles WHERE user_id = ?", userID)
database.DB.Exec("DELETE FROM social_accounts WHERE user_id = ?", userID)
// Permanently delete user
result := database.DB.Unscoped().Delete(&models.User{}, "id = ?", userID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrUserNotFound
}
return nil
}
// Soft delete
result := database.DB.Delete(&models.User{}, "id = ?", userID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrUserNotFound
}
return nil
}
// AssignRoles - Kullanıcıya roller ata
func (s *UserManagementService) AssignRoles(userID string, roleNames []string) error {
var user models.User
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
return err
}
var roles []models.Role
if err := database.DB.Where("name IN ?", roleNames).Find(&roles).Error; err != nil {
return err
}
if len(roles) == 0 {
return errors.New("no valid roles found")
}
return database.DB.Model(&user).Association("Roles").Replace(roles)
}
// RemoveRole - Kullanıcıdan rol kaldır
func (s *UserManagementService) RemoveRole(userID string, roleName string) error {
var user models.User
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
return err
}
var role models.Role
if err := database.DB.Where("name = ?", roleName).First(&role).Error; err != nil {
return err
}
return database.DB.Model(&user).Association("Roles").Delete(&role)
}
// SearchUsers - Kullanıcı ara (email, username)
func (s *UserManagementService) SearchUsers(query string, page, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
searchQuery := "%" + query + "%"
// Count total matching users
database.DB.Model(&models.User{}).
Where("email ILIKE ? OR user_name ILIKE ?", searchQuery, searchQuery).
Count(&total)
// Calculate offset
offset := (page - 1) * limit
// Fetch users
err := database.DB.
Preload("Roles").
Preload("SocialAccounts").
Where("email ILIKE ? OR user_name ILIKE ?", searchQuery, searchQuery).
Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&users).Error
return users, total, err
}
// GetDeletedUsers - Soft delete edilmiş kullanıcıları getir
func (s *UserManagementService) GetDeletedUsers(page, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
// Count total deleted users
database.DB.Model(&models.User{}).Unscoped().Where("deleted_at IS NOT NULL").Count(&total)
// Calculate offset
offset := (page - 1) * limit
// Fetch deleted users with pagination
err := database.DB.Unscoped().
Preload("Roles").
Preload("SocialAccounts").
Where("deleted_at IS NOT NULL").
Offset(offset).
Limit(limit).
Order("deleted_at DESC").
Find(&users).Error
return users, total, err
}
// RestoreUser - Soft delete edilmiş kullanıcıyı geri yükle
func (s *UserManagementService) RestoreUser(userID string) error {
var user models.User
// Find soft deleted user
if err := database.DB.Unscoped().Where("id = ? AND deleted_at IS NOT NULL", userID).First(&user).Error; err != nil {
return errors.New("deleted user not found")
}
// Restore user (set deleted_at to NULL)
return database.DB.Unscoped().Model(&user).Update("deleted_at", nil).Error
}

View File

@@ -0,0 +1,19 @@
package services
import (
"strings"
"github.com/google/uuid"
)
func parseUUIDPtr(value string) (*uuid.UUID, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
parsed, err := uuid.Parse(value)
if err != nil {
return nil, err
}
return &parsed, nil
}