first commit
This commit is contained in:
264
app/settings/handlers/settings_handler.go
Normal file
264
app/settings/handlers/settings_handler.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"gobeyhan/app/settings/services"
|
||||
"gobeyhan/database/models"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SettingsHandler struct {
|
||||
service *services.SettingsService
|
||||
}
|
||||
|
||||
func NewSettingsHandler(service *services.SettingsService) *SettingsHandler {
|
||||
return &SettingsHandler{service: service}
|
||||
}
|
||||
|
||||
// GetAllWhitelist godoc
|
||||
// @Summary Get all CORS whitelist entries (Admin)
|
||||
// @Description Get all CORS whitelist origins
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} models.CorsWhitelist
|
||||
// @Router /api/v1/admin/cors/whitelist [get]
|
||||
func (h *SettingsHandler) GetAllWhitelist(c *gin.Context) {
|
||||
whitelist, err := h.service.GetAllCorsWhitelist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": whitelist})
|
||||
}
|
||||
|
||||
// CreateWhitelist godoc
|
||||
// @Summary Create CORS whitelist entry (Admin)
|
||||
// @Description Add a new origin to CORS whitelist
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
|
||||
// @Success 201 {object} models.CorsWhitelist
|
||||
// @Router /api/v1/admin/cors/whitelist [post]
|
||||
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
|
||||
var input models.CorsWhitelist
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.CreateCorsWhitelist(&input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": input})
|
||||
}
|
||||
|
||||
// UpdateWhitelist godoc
|
||||
// @Summary Update CORS whitelist entry (Admin)
|
||||
// @Description Update an existing CORS whitelist entry
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Whitelist ID"
|
||||
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/cors/whitelist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.UpdateCorsWhitelist(id, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteWhitelist godoc
|
||||
// @Summary Delete CORS whitelist entry (Admin)
|
||||
// @Description Delete a CORS whitelist entry
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Whitelist ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/cors/whitelist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := h.service.DeleteCorsWhitelist(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Whitelist deleted successfully"})
|
||||
}
|
||||
|
||||
// GetAllBlacklist godoc
|
||||
// @Summary Get all CORS blacklist entries (Admin)
|
||||
// @Description Get all CORS blacklist origins
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} models.CorsBlacklist
|
||||
// @Router /api/v1/admin/cors/blacklist [get]
|
||||
func (h *SettingsHandler) GetAllBlacklist(c *gin.Context) {
|
||||
blacklist, err := h.service.GetAllCorsBlacklist()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": blacklist})
|
||||
}
|
||||
|
||||
// CreateBlacklist godoc
|
||||
// @Summary Create CORS blacklist entry (Admin)
|
||||
// @Description Add a new origin to CORS blacklist
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
|
||||
// @Success 201 {object} models.CorsBlacklist
|
||||
// @Router /api/v1/admin/cors/blacklist [post]
|
||||
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
|
||||
var input models.CorsBlacklist
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.CreateCorsBlacklist(&input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": input})
|
||||
}
|
||||
|
||||
// UpdateBlacklist godoc
|
||||
// @Summary Update CORS blacklist entry (Admin)
|
||||
// @Description Update an existing CORS blacklist entry
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Blacklist ID"
|
||||
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/cors/blacklist/{id} [put]
|
||||
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.UpdateCorsBlacklist(id, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteBlacklist godoc
|
||||
// @Summary Delete CORS blacklist entry (Admin)
|
||||
// @Description Delete a CORS blacklist entry
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Blacklist ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/cors/blacklist/{id} [delete]
|
||||
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := h.service.DeleteCorsBlacklist(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Blacklist deleted successfully"})
|
||||
}
|
||||
|
||||
// GetAllRateLimits godoc
|
||||
// @Summary Get all rate limit settings (Admin)
|
||||
// @Description Get all rate limit configurations
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} models.RateLimitSetting
|
||||
// @Router /api/v1/admin/rate-limits [get]
|
||||
func (h *SettingsHandler) GetAllRateLimits(c *gin.Context) {
|
||||
settings, err := h.service.GetAllRateLimitSettings()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": settings})
|
||||
}
|
||||
|
||||
// UpdateRateLimit godoc
|
||||
// @Summary Update rate limit setting (Admin)
|
||||
// @Description Update an existing rate limit configuration
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Rate Limit ID"
|
||||
// @Param setting body models.RateLimitSetting true "Rate limit object"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/rate-limits/{id} [put]
|
||||
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.UpdateRateLimitSetting(id, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Rate limit updated successfully"})
|
||||
}
|
||||
|
||||
// InvalidateCorsCache godoc
|
||||
// @Summary Invalidate CORS cache (Admin)
|
||||
// @Description Clear the CORS cache to force reload from database
|
||||
// @Tags admin,settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/admin/cors/cache/invalidate [post]
|
||||
func (h *SettingsHandler) InvalidateCorsCache(c *gin.Context) {
|
||||
h.service.InvalidateCorsCache()
|
||||
c.JSON(http.StatusOK, gin.H{"message": "CORS cache invalidated successfully"})
|
||||
}
|
||||
204
app/settings/services/cache_service.go
Normal file
204
app/settings/services/cache_service.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gobeyhan/database"
|
||||
"gobeyhan/database/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")
|
||||
}
|
||||
173
app/settings/services/jwt_service.go
Normal file
173
app/settings/services/jwt_service.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gobeyhan/config"
|
||||
"gobeyhan/database/models"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type JWTClaim struct {
|
||||
UserID string `json:"user_id"`
|
||||
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) {
|
||||
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||
return "", errors.New("jwt secret not configured")
|
||||
}
|
||||
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &JWTClaim{
|
||||
UserID: fmt.Sprintf("%d", user.ID),
|
||||
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) {
|
||||
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||
return "", "", errors.New("jwt secret not configured")
|
||||
}
|
||||
|
||||
// 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
|
||||
expirationMinutes := 120 // Default fallback
|
||||
if config.AppConfig.AccessTokenExpireMinutes > 0 {
|
||||
expirationMinutes = config.AppConfig.AccessTokenExpireMinutes
|
||||
}
|
||||
accessTokenExp := time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
|
||||
accessClaims := &JWTClaim{
|
||||
UserID: fmt.Sprintf("%d", user.ID),
|
||||
Email: user.Email,
|
||||
Permissions: permissions,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
|
||||
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
|
||||
expirationDays := 30 // Default fallback
|
||||
if config.AppConfig.RefreshTokenExpireDays > 0 {
|
||||
expirationDays = config.AppConfig.RefreshTokenExpireDays
|
||||
}
|
||||
refreshTokenExp := time.Now().Add(time.Duration(expirationDays) * 24 * time.Hour)
|
||||
refreshClaims := &JWTClaim{
|
||||
UserID: fmt.Sprintf("%d", user.ID),
|
||||
Email: user.Email,
|
||||
Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
|
||||
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) {
|
||||
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||
return nil, errors.New("jwt secret not configured")
|
||||
}
|
||||
|
||||
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) {
|
||||
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||
return "", errors.New("jwt secret not configured")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
391
app/settings/services/settings_service.go
Normal file
391
app/settings/services/settings_service.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gobeyhan/database"
|
||||
"gobeyhan/database/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
|
||||
}
|
||||
|
||||
func (s *SettingsService) GetCorsWhitelistByID(id uint64) (*models.CorsWhitelist, error) {
|
||||
var item models.CorsWhitelist
|
||||
err := database.DB.First(&item, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, 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
|
||||
}
|
||||
|
||||
func (s *SettingsService) GetCorsBlacklistByID(id uint64) (*models.CorsBlacklist, error) {
|
||||
var item models.CorsBlacklist
|
||||
err := database.DB.First(&item, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, 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
|
||||
}
|
||||
|
||||
func (s *SettingsService) GetRateLimitSettingByID(id uint64) (*models.RateLimitSetting, error) {
|
||||
var item models.RateLimitSetting
|
||||
err := database.DB.First(&item, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (s *SettingsService) DeleteRateLimitSetting(id string) error {
|
||||
err := database.DB.Delete(&models.RateLimitSetting{}, "id = ?", id).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
|
||||
}
|
||||
Reference in New Issue
Block a user