first commit
This commit is contained in:
330
internal/services/about_service.go
Normal file
330
internal/services/about_service.go
Normal 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
|
||||
}
|
||||
240
internal/services/auth_service.go
Normal file
240
internal/services/auth_service.go
Normal 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
|
||||
}
|
||||
172
internal/services/banner_service.go
Normal file
172
internal/services/banner_service.go
Normal 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
|
||||
}
|
||||
204
internal/services/cache_service.go
Normal file
204
internal/services/cache_service.go
Normal 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")
|
||||
}
|
||||
95
internal/services/contact_service.go
Normal file
95
internal/services/contact_service.go
Normal 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
|
||||
}
|
||||
103
internal/services/education_service.go
Normal file
103
internal/services/education_service.go
Normal 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
|
||||
}
|
||||
103
internal/services/experience_service.go
Normal file
103
internal/services/experience_service.go
Normal 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
|
||||
}
|
||||
263
internal/services/home_service.go
Normal file
263
internal/services/home_service.go
Normal 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
|
||||
}
|
||||
146
internal/services/jwt_service.go
Normal file
146
internal/services/jwt_service.go
Normal 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
|
||||
}
|
||||
95
internal/services/knowledge_service.go
Normal file
95
internal/services/knowledge_service.go
Normal 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
|
||||
}
|
||||
134
internal/services/main_menu_service.go
Normal file
134
internal/services/main_menu_service.go
Normal 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
|
||||
}
|
||||
214
internal/services/post_category_service.go
Normal file
214
internal/services/post_category_service.go
Normal 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
|
||||
}
|
||||
65
internal/services/post_category_view_service.go
Normal file
65
internal/services/post_category_view_service.go
Normal 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
|
||||
}
|
||||
200
internal/services/post_comment_service.go
Normal file
200
internal/services/post_comment_service.go
Normal 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
|
||||
}
|
||||
297
internal/services/post_service.go
Normal file
297
internal/services/post_service.go
Normal 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
|
||||
}
|
||||
87
internal/services/post_tag_service.go
Normal file
87
internal/services/post_tag_service.go
Normal 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
|
||||
}
|
||||
184
internal/services/resume_service.go
Normal file
184
internal/services/resume_service.go
Normal 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
|
||||
}
|
||||
193
internal/services/service_service.go
Normal file
193
internal/services/service_service.go
Normal 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
|
||||
}
|
||||
134
internal/services/service_title_service.go
Normal file
134
internal/services/service_title_service.go
Normal 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
|
||||
}
|
||||
353
internal/services/settings_service.go
Normal file
353
internal/services/settings_service.go
Normal 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
|
||||
}
|
||||
245
internal/services/site_info_service.go
Normal file
245
internal/services/site_info_service.go
Normal 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
|
||||
}
|
||||
109
internal/services/site_settings_service.go
Normal file
109
internal/services/site_settings_service.go
Normal 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
|
||||
}
|
||||
99
internal/services/skill_service.go
Normal file
99
internal/services/skill_service.go
Normal 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
|
||||
}
|
||||
91
internal/services/tag_service.go
Normal file
91
internal/services/tag_service.go
Normal 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
|
||||
}
|
||||
257
internal/services/user_management_service.go
Normal file
257
internal/services/user_management_service.go
Normal 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
|
||||
}
|
||||
19
internal/services/uuid_helpers.go
Normal file
19
internal/services/uuid_helpers.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user