first commit
This commit is contained in:
307
internal/database/db.go
Normal file
307
internal/database/db.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gauth-central/config"
|
||||
"gauth-central/internal/models"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func ConnectDB() {
|
||||
dsn := config.AppConfig.DBUrl
|
||||
if dsn == "" {
|
||||
log.Fatal("DB_URL is not set in .env")
|
||||
}
|
||||
|
||||
// Configure GORM with optimized settings
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Error), // Only log errors, suppress SLOW SQL warnings
|
||||
PrepareStmt: true, // Prepare statements for better performance
|
||||
NowFunc: func() time.Time {
|
||||
return time.Now().UTC()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
log.Println("Connected to Database successfully")
|
||||
DB = db
|
||||
|
||||
// Enable UUID extension
|
||||
enableUUIDExtension()
|
||||
}
|
||||
|
||||
func enableUUIDExtension() {
|
||||
// Enable uuid-ossp extension for uuid_generate_v4()
|
||||
err := DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error
|
||||
if err != nil {
|
||||
log.Printf("Warning: Could not enable uuid-ossp extension: %v", err)
|
||||
} else {
|
||||
log.Println("UUID extension enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func Migrate() {
|
||||
// Manual migration for user_name column to handle existing data
|
||||
migrateUserNameColumn()
|
||||
|
||||
err := DB.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.SocialAccount{},
|
||||
&models.Role{},
|
||||
&models.Permission{},
|
||||
&models.CorsWhitelist{},
|
||||
&models.CorsBlacklist{},
|
||||
&models.RateLimitSetting{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("Database Migration Failed:", err)
|
||||
}
|
||||
log.Println("Database Migration Completed")
|
||||
|
||||
// Migration for email_verified column is disabled after initial setup
|
||||
// New users will have email_verified=false by default for email/password registration
|
||||
// migrateEmailVerifiedColumn()
|
||||
|
||||
seedRolesAndPermissions()
|
||||
seedDefaultSettings()
|
||||
// seedDefaultAdmin() - Removed from auto migration
|
||||
}
|
||||
|
||||
func migrateEmailVerifiedColumn() {
|
||||
// Fast check using pg_catalog instead of information_schema
|
||||
var count int64
|
||||
DB.Raw(`
|
||||
SELECT COUNT(*)
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'users'::regclass
|
||||
AND attname = 'email_verified'
|
||||
AND NOT attisdropped
|
||||
`).Scan(&count)
|
||||
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Only set existing users (created before email verification feature) as verified
|
||||
// Users with no verify token AND created before a certain date are old users
|
||||
// For simplicity: set all users without a verification token as verified (one-time migration)
|
||||
var usersToVerify int64
|
||||
DB.Model(&models.User{}).Where("(email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL").Count(&usersToVerify)
|
||||
|
||||
if usersToVerify > 0 {
|
||||
DB.Exec("UPDATE users SET email_verified = true WHERE (email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL")
|
||||
log.Printf("Email verification migration: %d existing users marked as verified", usersToVerify)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateUserNameColumn() {
|
||||
// Fast check using pg_catalog instead of information_schema
|
||||
var count int64
|
||||
DB.Raw(`
|
||||
SELECT COUNT(*)
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'users'::regclass
|
||||
AND attname = 'user_name'
|
||||
AND NOT attisdropped
|
||||
`).Scan(&count)
|
||||
|
||||
if count == 0 {
|
||||
// Column doesn't exist, add it
|
||||
log.Println("Adding user_name column...")
|
||||
DB.Exec("ALTER TABLE users ADD COLUMN user_name text")
|
||||
|
||||
// Update existing users with default usernames
|
||||
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(id::text, 1, 8)) WHERE user_name IS NULL")
|
||||
|
||||
// Add NOT NULL constraint
|
||||
DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET NOT NULL")
|
||||
log.Println("user_name column added successfully")
|
||||
} else {
|
||||
// Column exists, update null values
|
||||
log.Println("Updating users with null usernames...")
|
||||
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(id::text, 1, 8)) WHERE user_name IS NULL OR user_name = ''")
|
||||
|
||||
// Check if NOT NULL constraint exists using pg_catalog
|
||||
var isNotNull bool
|
||||
DB.Raw(`
|
||||
SELECT attnotnull
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = 'users'::regclass
|
||||
AND attname = 'user_name'
|
||||
`).Scan(&isNotNull)
|
||||
|
||||
if !isNotNull {
|
||||
log.Println("Adding NOT NULL constraint to user_name...")
|
||||
DB.Exec("ALTER TABLE users ALTER COLUMN user_name SET NOT NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func seedRolesAndPermissions() {
|
||||
// 1. Define Permissions
|
||||
permissions := []models.Permission{
|
||||
{Name: "user:read", Description: "Can read user data"},
|
||||
{Name: "user:write", Description: "Can modify user data"},
|
||||
{Name: "admin:access", Description: "Can access admin panel"},
|
||||
}
|
||||
|
||||
for _, p := range permissions {
|
||||
DB.FirstOrCreate(&models.Permission{}, models.Permission{Name: p.Name, Description: p.Description})
|
||||
}
|
||||
|
||||
// 2. Define Roles
|
||||
roles := []string{"admin", "user"}
|
||||
for _, r := range roles {
|
||||
DB.FirstOrCreate(&models.Role{}, models.Role{Name: r, Description: "Default " + r + " role"})
|
||||
}
|
||||
|
||||
// 3. Assign Permissions to Admin Role
|
||||
var adminRole models.Role
|
||||
DB.Preload("Permissions").Where("name = ?", "admin").First(&adminRole)
|
||||
|
||||
// Fetch all permissions to assign to admin
|
||||
var allPermissions []models.Permission
|
||||
DB.Find(&allPermissions)
|
||||
|
||||
// Update association (append missing ones)
|
||||
DB.Model(&adminRole).Association("Permissions").Replace(allPermissions)
|
||||
|
||||
// 4. Assign Basic Permissions to User Role
|
||||
var userRole models.Role
|
||||
DB.Preload("Permissions").Where("name = ?", "user").First(&userRole)
|
||||
|
||||
var userPermissions []models.Permission
|
||||
DB.Where("name IN ?", []string{"user:read"}).Find(&userPermissions)
|
||||
|
||||
DB.Model(&userRole).Association("Permissions").Replace(userPermissions)
|
||||
|
||||
log.Println("Roles and Permissions seeded")
|
||||
}
|
||||
|
||||
func seedDefaultSettings() {
|
||||
// Seed default CORS whitelist
|
||||
var whitelistCount int64
|
||||
DB.Model(&models.CorsWhitelist{}).Count(&whitelistCount)
|
||||
|
||||
if whitelistCount == 0 {
|
||||
defaultWhitelist := []models.CorsWhitelist{
|
||||
{
|
||||
Origin: "http://localhost:3000",
|
||||
Description: "Default local frontend",
|
||||
IsActive: true,
|
||||
CreatedBy: "system",
|
||||
},
|
||||
{
|
||||
Origin: "http://localhost:8080",
|
||||
Description: "Backend self",
|
||||
IsActive: true,
|
||||
CreatedBy: "system",
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range defaultWhitelist {
|
||||
DB.Create(&w)
|
||||
}
|
||||
log.Println("Default CORS whitelist seeded")
|
||||
}
|
||||
|
||||
// Seed default rate limit settings
|
||||
var rateLimitCount int64
|
||||
DB.Model(&models.RateLimitSetting{}).Count(&rateLimitCount)
|
||||
|
||||
if rateLimitCount == 0 {
|
||||
defaultRateLimits := []models.RateLimitSetting{
|
||||
{
|
||||
Name: "login",
|
||||
Description: "Login endpoint rate limit",
|
||||
MaxRequests: 5,
|
||||
WindowSeconds: 60, // 1 minute
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
Name: "register",
|
||||
Description: "Registration endpoint rate limit",
|
||||
MaxRequests: 3,
|
||||
WindowSeconds: 300, // 5 minutes
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
Name: "api",
|
||||
Description: "General API rate limit",
|
||||
MaxRequests: 100,
|
||||
WindowSeconds: 60, // 1 minute
|
||||
IsActive: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, r := range defaultRateLimits {
|
||||
DB.Create(&r)
|
||||
}
|
||||
log.Println("Default rate limit settings seeded")
|
||||
}
|
||||
}
|
||||
|
||||
// SeedDefaultAdmin creates the default admin user if it doesn't exist
|
||||
func SeedDefaultAdmin() {
|
||||
// Check if admin user already exists (including soft-deleted)
|
||||
var adminUser models.User
|
||||
err := DB.Unscoped().Where("email = ?", "admin@gauth.local").First(&adminUser).Error
|
||||
|
||||
if err != nil {
|
||||
// Admin user doesn't exist, create one
|
||||
// Hash default password: "Admin@123"
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("Admin@123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("Failed to hash admin password: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
trueBool := true
|
||||
adminUser = models.User{
|
||||
Email: "admin@gauth.local",
|
||||
UserName: "admin",
|
||||
Password: string(hashedPassword),
|
||||
EmailVerified: &trueBool,
|
||||
}
|
||||
|
||||
if err := DB.Create(&adminUser).Error; err != nil {
|
||||
log.Printf("Failed to create admin user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("✅ Default admin user created:")
|
||||
log.Println(" Email: admin@gauth.local")
|
||||
log.Println(" Password: Admin@123")
|
||||
log.Println(" ⚠️ Please change this password after first login!")
|
||||
} else {
|
||||
// Admin user exists (possibly soft-deleted)
|
||||
if adminUser.DeletedAt.Valid {
|
||||
log.Println("Restoring deleted admin user...")
|
||||
if err := DB.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil {
|
||||
log.Printf("Failed to restore admin user: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure admin role is assigned
|
||||
var adminRole models.Role
|
||||
if err := DB.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
log.Printf("Admin role not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := DB.Model(&adminUser).Association("Roles").Append(&adminRole); err != nil {
|
||||
log.Printf("Failed to assign admin role: %v", err)
|
||||
}
|
||||
}
|
||||
97
internal/database/redis.go
Normal file
97
internal/database/redis.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gauth-central/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var RedisClient *redis.Client
|
||||
var ctx = context.Background()
|
||||
|
||||
func ConnectRedis() {
|
||||
redisURL := config.AppConfig.RedisUrl
|
||||
if redisURL == "" {
|
||||
log.Println("Warning: REDIS_URL is not set, continuing without Redis cache")
|
||||
return
|
||||
}
|
||||
|
||||
opt, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to parse Redis URL: %v, continuing without Redis cache", err)
|
||||
return
|
||||
}
|
||||
|
||||
RedisClient = redis.NewClient(opt)
|
||||
|
||||
// Test connection
|
||||
_, err = RedisClient.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
|
||||
RedisClient = nil
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Connected to Redis successfully")
|
||||
}
|
||||
|
||||
// Set stores a key-value pair in Redis with expiration
|
||||
func Set(key string, value interface{}, expiration time.Duration) error {
|
||||
if RedisClient == nil {
|
||||
return nil // Gracefully handle when Redis is not available
|
||||
}
|
||||
return RedisClient.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
// Get retrieves a value from Redis
|
||||
func Get(key string) (string, error) {
|
||||
if RedisClient == nil {
|
||||
return "", redis.Nil // Return Nil error when Redis is not available
|
||||
}
|
||||
return RedisClient.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Delete removes a key from Redis
|
||||
func Delete(key string) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
// Exists checks if a key exists in Redis
|
||||
func Exists(key string) (bool, error) {
|
||||
if RedisClient == nil {
|
||||
return false, nil
|
||||
}
|
||||
count, err := RedisClient.Exists(ctx, key).Result()
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// SetWithJSON stores a JSON-serializable value in Redis
|
||||
func SetEx(key string, value interface{}, seconds int) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Set(ctx, key, value, time.Duration(seconds)*time.Second).Err()
|
||||
}
|
||||
|
||||
// Increment increments a counter in Redis
|
||||
func Increment(key string) (int64, error) {
|
||||
if RedisClient == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return RedisClient.Incr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// Expire sets expiration time for a key
|
||||
func Expire(key string, expiration time.Duration) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
67
internal/models/cors_setting.go
Normal file
67
internal/models/cors_setting.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CorsWhitelist - CORS için izin verilen origin'ler
|
||||
type CorsWhitelist struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate - UUID otomatik oluşturma
|
||||
func (c *CorsWhitelist) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == uuid.Nil {
|
||||
c.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CorsBlacklist - CORS için yasaklanan origin'ler
|
||||
type CorsBlacklist struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Reason string `gorm:"type:text" json:"reason"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate - UUID otomatik oluşturma
|
||||
func (c *CorsBlacklist) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == uuid.Nil {
|
||||
c.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RateLimitSetting - Rate limit ayarları
|
||||
type RateLimitSetting struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
MaxRequests int64 `gorm:"not null" json:"max_requests"` // Max istek sayısı
|
||||
WindowSeconds int `gorm:"not null" json:"window_seconds"` // Zaman penceresi (saniye)
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
UpdatedBy string `gorm:"type:varchar(255)" json:"updated_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate - UUID otomatik oluşturma
|
||||
func (r *RateLimitSetting) BeforeCreate(tx *gorm.DB) error {
|
||||
if r.ID == uuid.Nil {
|
||||
r.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
internal/models/role.go
Normal file
14
internal/models/role.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
type Role struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;not null" json:"name"` // admin, user
|
||||
Description string `json:"description"`
|
||||
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions"`
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;not null" json:"name"` // user:read, user:write
|
||||
Description string `json:"description"`
|
||||
}
|
||||
52
internal/models/user.go
Normal file
52
internal/models/user.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User model structure
|
||||
type User struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||
UserName string `json:"username"`
|
||||
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||
Password string `json:"-"` // Password shouldn't be returned in JSON
|
||||
Avatar string `gorm:"type:varchar(500)" json:"avatar,omitempty"` // Avatar URL from OAuth or uploaded
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Email verification: only required for email/password registration; OAuth users are treated as verified
|
||||
// Changed to *bool to handle false values correctly with GORM defaults
|
||||
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
|
||||
EmailVerifyToken string `gorm:"index" json:"-"`
|
||||
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
|
||||
|
||||
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
|
||||
Roles []Role `gorm:"many2many:user_roles;" json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// Helper to safely get EmailVerified status
|
||||
func (u *User) IsEmailVerified() bool {
|
||||
if u.EmailVerified == nil {
|
||||
return false
|
||||
}
|
||||
return *u.EmailVerified
|
||||
}
|
||||
|
||||
// SocialAccount model structure
|
||||
type SocialAccount struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||
UserID uuid.UUID `gorm:"not null" json:"user_id"`
|
||||
Provider string `gorm:"not null" json:"provider"` // google, github
|
||||
ProviderID string `gorm:"not null" json:"provider_id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name,omitempty"` // Full name from provider
|
||||
AvatarURL string `json:"avatar_url,omitempty"` // Avatar URL from provider
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Hooks can be added here if needed
|
||||
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
|
||||
}
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
236
internal/services/settings_service.go
Normal file
236
internal/services/settings_service.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Fetch from database
|
||||
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
|
||||
}
|
||||
|
||||
// Cache for 1 hour
|
||||
s.cacheService.SetCorsWhitelist(origins, 1*time.Hour)
|
||||
|
||||
return origins, nil
|
||||
}
|
||||
|
||||
func (s *SettingsService) CreateCorsWhitelist(whitelist *models.CorsWhitelist) error {
|
||||
err := database.DB.Create(whitelist).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate cache
|
||||
s.cacheService.InvalidateCorsWhitelist()
|
||||
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.cacheService.InvalidateCorsWhitelist()
|
||||
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.cacheService.InvalidateCorsWhitelist()
|
||||
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
|
||||
}
|
||||
|
||||
// Fetch from database
|
||||
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
|
||||
}
|
||||
|
||||
// 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.cacheService.InvalidateCorsBlacklist()
|
||||
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.cacheService.InvalidateCorsBlacklist()
|
||||
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.cacheService.InvalidateCorsBlacklist()
|
||||
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
|
||||
}
|
||||
|
||||
// Check if origin is allowed
|
||||
func (s *SettingsService) IsOriginAllowed(origin string) (bool, error) {
|
||||
// Check blacklist first
|
||||
blacklist, err := s.GetActiveBlacklistOrigins()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, blocked := range blacklist {
|
||||
if blocked == origin {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check whitelist
|
||||
whitelist, err := s.GetActiveWhitelistOrigins()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, allowed := range whitelist {
|
||||
if allowed == origin || allowed == "*" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
237
internal/services/user_management_service.go
Normal file
237
internal/services/user_management_service.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gauth-central/internal/database"
|
||||
"gauth-central/internal/models"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserManagementService struct{}
|
||||
|
||||
func NewUserManagementService() *UserManagementService {
|
||||
return &UserManagementService{}
|
||||
}
|
||||
|
||||
// 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.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
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
|
||||
return database.DB.Unscoped().Delete(&models.User{}, "id = ?", userID).Error
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
return database.DB.Delete(&models.User{}, "id = ?", userID).Error
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user